summaryrefslogtreecommitdiff
path: root/internal/llmutils/client.go
blob: a68746edd19607b4293bb3abd41a99c6c639b969 (plain)
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)
}