summaryrefslogtreecommitdiff
path: root/internal/llm
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-17 22:49:13 +0300
committerPaul Buetow <paul@buetow.org>2025-09-17 22:49:13 +0300
commitd059ae333fa1c89cb58d7fb56ead79cdba15d5db (patch)
treeae65ad59c8590f71232a6abefee312b72ddf6d3e /internal/llm
parent88103657fb230bb41217a06aa5602ae23e7acb8b (diff)
chore(version): bump to v0.11.1 (gpt-5 defaults, timeouts, global stats, editor fix)v0.11.1
Diffstat (limited to 'internal/llm')
-rw-r--r--internal/llm/openai.go27
-rw-r--r--internal/llm/openai_request_test.go32
-rw-r--r--internal/llm/openai_temp_test.go43
-rw-r--r--internal/llm/provider.go22
4 files changed, 113 insertions, 11 deletions
diff --git a/internal/llm/openai.go b/internal/llm/openai.go
index e9a1fdc..8b00335 100644
--- a/internal/llm/openai.go
+++ b/internal/llm/openai.go
@@ -26,12 +26,13 @@ type openAIClient struct {
}
type oaChatRequest struct {
- Model string `json:"model"`
- Messages []oaMessage `json:"messages"`
- Temperature *float64 `json:"temperature,omitempty"`
- MaxTokens *int `json:"max_tokens,omitempty"`
- Stop []string `json:"stop,omitempty"`
- Stream bool `json:"stream,omitempty"`
+ Model string `json:"model"`
+ Messages []oaMessage `json:"messages"`
+ Temperature *float64 `json:"temperature,omitempty"`
+ MaxTokens *int `json:"max_tokens,omitempty"`
+ MaxCompletionTokens *int `json:"max_completion_tokens,omitempty"`
+ Stop []string `json:"stop,omitempty"`
+ Stream bool `json:"stream,omitempty"`
}
type oaMessage struct {
@@ -208,7 +209,11 @@ func buildOAChatRequest(o Options, messages []Message, defaultTemp *float64, str
req.Temperature = &t
}
if o.MaxTokens > 0 {
- req.MaxTokens = &o.MaxTokens
+ if requiresMaxCompletionTokens(o.Model) {
+ req.MaxCompletionTokens = &o.MaxTokens
+ } else {
+ req.MaxTokens = &o.MaxTokens
+ }
}
if len(o.Stop) > 0 {
req.Stop = o.Stop
@@ -216,6 +221,14 @@ func buildOAChatRequest(o Options, messages []Message, defaultTemp *float64, str
return req
}
+// requiresMaxCompletionTokens reports whether the given model prefers the
+// new parameter name "max_completion_tokens" instead of "max_tokens". Newer
+// models (e.g., gpt-5 family) expect this per OpenAI's API error guidance.
+func requiresMaxCompletionTokens(model string) bool {
+ m := strings.ToLower(strings.TrimSpace(model))
+ return strings.HasPrefix(m, "gpt-5")
+}
+
func (c openAIClient) doJSON(ctx context.Context, url string, body []byte, headers map[string]string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
diff --git a/internal/llm/openai_request_test.go b/internal/llm/openai_request_test.go
new file mode 100644
index 0000000..f9925f9
--- /dev/null
+++ b/internal/llm/openai_request_test.go
@@ -0,0 +1,32 @@
+package llm
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestBuildOAChatRequest_MaxTokensKeyByModel(t *testing.T) {
+ msgs := []Message{{Role: "user", Content: "hi"}}
+ mt := 123
+ // Legacy model: use max_tokens
+ r1 := buildOAChatRequest(Options{Model: "gpt-4.1", MaxTokens: mt}, msgs, nil, false)
+ b1, _ := json.Marshal(r1)
+ if !contains(string(b1), "max_tokens") || contains(string(b1), "max_completion_tokens") {
+ t.Fatalf("expected max_tokens only, got %s", string(b1))
+ }
+ // gpt-5 family: use max_completion_tokens
+ r2 := buildOAChatRequest(Options{Model: "gpt-5.0-preview", MaxTokens: mt}, msgs, nil, false)
+ b2, _ := json.Marshal(r2)
+ if !contains(string(b2), "max_completion_tokens") || contains(string(b2), "max_tokens\":") {
+ t.Fatalf("expected max_completion_tokens only, got %s", string(b2))
+ }
+}
+
+func contains(s, sub string) bool {
+ for i := 0; i+len(sub) <= len(s); i++ {
+ if s[i:i+len(sub)] == sub {
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/llm/openai_temp_test.go b/internal/llm/openai_temp_test.go
new file mode 100644
index 0000000..7615117
--- /dev/null
+++ b/internal/llm/openai_temp_test.go
@@ -0,0 +1,43 @@
+package llm
+
+import "testing"
+
+func TestNewFromConfig_DefaultTemp_ByModel(t *testing.T) {
+ // OpenAI, gpt-5.* → default temp 1.0 when not provided
+ cfg := Config{Provider: "openai", OpenAIModel: "gpt-5.0-preview"}
+ c, err := NewFromConfig(cfg, "key", "")
+ if err != nil {
+ t.Fatalf("new: %v", err)
+ }
+ oc, ok := c.(openAIClient)
+ if !ok {
+ t.Fatalf("expected openAIClient")
+ }
+ if oc.defaultTemperature == nil || *oc.defaultTemperature != 1.0 {
+ t.Fatalf("expected default temp 1.0 for gpt-5, got %#v", oc.defaultTemperature)
+ }
+ // OpenAI, gpt-4.* → default temp 0.2 when not provided
+ cfg2 := Config{Provider: "openai", OpenAIModel: "gpt-4.1"}
+ c2, err := NewFromConfig(cfg2, "key", "")
+ if err != nil {
+ t.Fatalf("new2: %v", err)
+ }
+ oc2 := c2.(openAIClient)
+ if oc2.defaultTemperature == nil || *oc2.defaultTemperature != 0.2 {
+ t.Fatalf("expected default temp 0.2 for gpt-4.*, got %#v", oc2.defaultTemperature)
+ }
+}
+
+func TestNewFromConfig_DefaultTemp_UpgradeWhenGpt5AndDefault02(t *testing.T) {
+ // Simulate app-default of 0.2 while selecting a gpt-5 model: should upgrade to 1.0
+ v := 0.2
+ cfg := Config{Provider: "openai", OpenAIModel: "gpt-5.0", OpenAITemperature: &v}
+ c, err := NewFromConfig(cfg, "key", "")
+ if err != nil {
+ t.Fatalf("new: %v", err)
+ }
+ oc := c.(openAIClient)
+ if oc.defaultTemperature == nil || *oc.defaultTemperature != 1.0 {
+ t.Fatalf("expected upgraded default temp 1.0 for gpt-5 with default 0.2, got %#v", oc.defaultTemperature)
+ }
+}
diff --git a/internal/llm/provider.go b/internal/llm/provider.go
index 88c280c..84efaf9 100644
--- a/internal/llm/provider.go
+++ b/internal/llm/provider.go
@@ -92,10 +92,24 @@ func NewFromConfig(cfg Config, openAIAPIKey, copilotAPIKey string) (Client, erro
if strings.TrimSpace(openAIAPIKey) == "" {
return nil, errors.New("missing OPENAI_API_KEY for provider openai")
}
- // Set coding-friendly default temperature if none provided
- if cfg.OpenAITemperature == nil {
- t := 0.2
- cfg.OpenAITemperature = &t
+ // Default temperature selection:
+ // - When model is gpt-5*, prefer 1.0 by default (more exploratory).
+ // - Otherwise, prefer 0.2 by default (coding friendly).
+ // The app-wide defaults currently set provider temps to 0.2.
+ // If the user hasn't explicitly overridden and the model is gpt-5*,
+ // upgrade 0.2 → 1.0 to satisfy the requested default for gpt-5.
+ model := strings.ToLower(strings.TrimSpace(cfg.OpenAIModel))
+ if strings.HasPrefix(model, "gpt-5") {
+ if cfg.OpenAITemperature == nil {
+ v := 1.0
+ cfg.OpenAITemperature = &v
+ } else if *cfg.OpenAITemperature == 0.2 {
+ v := 1.0
+ cfg.OpenAITemperature = &v
+ }
+ } else if cfg.OpenAITemperature == nil {
+ v := 0.2
+ cfg.OpenAITemperature = &v
}
return newOpenAI(cfg.OpenAIBaseURL, cfg.OpenAIModel, openAIAPIKey, cfg.OpenAITemperature), nil
case "ollama":