diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-17 22:49:13 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-17 22:49:13 +0300 |
| commit | d059ae333fa1c89cb58d7fb56ead79cdba15d5db (patch) | |
| tree | ae65ad59c8590f71232a6abefee312b72ddf6d3e /internal/llm | |
| parent | 88103657fb230bb41217a06aa5602ae23e7acb8b (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.go | 27 | ||||
| -rw-r--r-- | internal/llm/openai_request_test.go | 32 | ||||
| -rw-r--r-- | internal/llm/openai_temp_test.go | 43 | ||||
| -rw-r--r-- | internal/llm/provider.go | 22 |
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": |
