1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
|
package llmutils
import (
"os"
"strings"
"codeberg.org/snonux/hexai/internal/appconfig"
"codeberg.org/snonux/hexai/internal/llm"
)
// CanonicalProvider normalizes provider names and defaults to ollama (Ollama
// Cloud at https://ollama.com when paired with the default base URL).
func CanonicalProvider(name string) string {
provider := strings.ToLower(strings.TrimSpace(name))
if provider == "" {
return "ollama"
}
return provider
}
// DefaultModelForProvider returns the configured default model for a provider.
func DefaultModelForProvider(cfg appconfig.App, provider string) string {
switch CanonicalProvider(provider) {
case "openrouter":
if model := strings.TrimSpace(cfg.OpenRouterModel); model != "" {
return model
}
return "openrouter/auto"
case "ollama":
if model := strings.TrimSpace(cfg.OllamaModel); model != "" {
return model
}
return "gemma4:31b-cloud"
case "anthropic":
if model := strings.TrimSpace(cfg.AnthropicModel); model != "" {
return model
}
return "claude-3-5-sonnet-20240620"
case "yousearch":
if effort := strings.TrimSpace(cfg.YouSearchResearchEffort); effort != "" {
return effort
}
return "standard"
default:
if model := strings.TrimSpace(cfg.OpenAIModel); model != "" {
return model
}
return "gpt-4.1"
}
}
// ConfigForProvider returns cfg adjusted for the selected provider/model.
func ConfigForProvider(cfg appconfig.App, provider, modelOverride string) appconfig.App {
derived := cfg
if strings.TrimSpace(provider) == "" {
provider = cfg.Provider
}
normalized := CanonicalProvider(provider)
derived.Provider = normalized
model := strings.TrimSpace(modelOverride)
if model == "" {
return derived
}
switch normalized {
case "openrouter":
derived.OpenRouterModel = model
case "ollama":
derived.OllamaModel = model
case "anthropic":
derived.AnthropicModel = model
default:
derived.OpenAIModel = model
}
return derived
}
// NewClientFromAppForProvider builds a client for a specific provider/model.
func NewClientFromAppForProvider(cfg appconfig.App, provider, modelOverride string) (llm.Client, error) {
return NewClientFromApp(ConfigForProvider(cfg, provider, modelOverride))
}
// NewClientFromApp builds an llm.Client using app config and environment keys.
func NewClientFromApp(cfg appconfig.App) (llm.Client, error) {
llmCfg := llm.Config{
Provider: cfg.Provider,
RequestTimeout: cfg.RequestTimeout,
OpenAIBaseURL: cfg.OpenAIBaseURL,
OpenAIModel: cfg.OpenAIModel,
OpenAITemperature: cfg.OpenAITemperature,
OpenRouterBaseURL: cfg.OpenRouterBaseURL,
OpenRouterModel: cfg.OpenRouterModel,
OpenRouterTemperature: cfg.OpenRouterTemperature,
OllamaBaseURL: cfg.OllamaBaseURL,
OllamaModel: cfg.OllamaModel,
OllamaTemperature: cfg.OllamaTemperature,
AnthropicBaseURL: cfg.AnthropicBaseURL,
AnthropicModel: cfg.AnthropicModel,
AnthropicTemperature: cfg.AnthropicTemperature,
YouSearchResearchEffort: cfg.YouSearchResearchEffort,
}
oaKey := os.Getenv("HEXAI_OPENAI_API_KEY")
if strings.TrimSpace(oaKey) == "" {
oaKey = os.Getenv("OPENAI_API_KEY")
}
orKey := os.Getenv("HEXAI_OPENROUTER_API_KEY")
if strings.TrimSpace(orKey) == "" {
orKey = os.Getenv("OPENROUTER_API_KEY")
}
anKey := os.Getenv("HEXAI_ANTHROPIC_API_KEY")
if strings.TrimSpace(anKey) == "" {
anKey = os.Getenv("ANTHROPIC_API_KEY")
}
// Ollama API key is optional: only needed for Ollama Cloud (ollama.ai).
// A local Ollama server keeps working when this is empty.
olKey := os.Getenv("HEXAI_OLLAMA_API_KEY")
if strings.TrimSpace(olKey) == "" {
olKey = os.Getenv("OLLAMA_API_KEY")
}
ysKey := os.Getenv("HEXAI_YOUSEARCH_API_KEY")
if strings.TrimSpace(ysKey) == "" {
ysKey = os.Getenv("YOU_API_KEY")
}
return llm.NewFromConfig(llmCfg, oaKey, orKey, anKey, olKey, ysKey)
}
|