From f296d3cb3c42bd3a16f254ab3ce92f79a3c82dea Mon Sep 17 00:00:00 2001 From: Erick Shaffer Date: Wed, 23 Sep 2026 20:38:51 -0600 Subject: [PATCH] chore: default OpenAI model to gpt-6-luna GPT-6 Luna is half the input price of gpt-5.6-luna ($0.10 vs $0.20 per 1M tokens) and ~58% cheaper on output ($0.50 vs $1.20), per the OpenAI API pricing page. Also fixes reasoning-model detection: the categorizer only treated `gpt-5*` as a reasoning model, so any `gpt-6-*` model would have been sent `temperature` instead of `reasoning_effort`. isGPT5Model is replaced by isReasoningModel, which treats GPT generation >= 5 as a reasoning model. Covered by TestIsReasoningModel and logged in docs/bug-fixes.md. --- .env.example | 2 +- AGENTS.md | 2 +- README.md | 2 +- config.yaml | 2 +- docs/bug-fixes.md | 13 +++++++++++ internal/domain/categorizer/categorizer.go | 20 ++++++++++++---- .../domain/categorizer/categorizer_test.go | 23 +++++++++++++++++++ internal/infrastructure/config/config.go | 2 +- internal/infrastructure/config/config_test.go | 4 ++-- 9 files changed, 59 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index 7986e49..7df9137 100644 --- a/.env.example +++ b/.env.example @@ -19,7 +19,7 @@ SENTRY_DSN=https://275ad5c45f7eb6454f31ae6a8c325f46@o4509941888122880.ingest.us. # OpenAI (default if no provider is forced) # OPENAI_API_KEY=your-openai-api-key -# OPENAI_MODEL=gpt-5.4-nano +# OPENAI_MODEL=gpt-6-luna # Anthropic (Claude) # ANTHROPIC_API_KEY=your-anthropic-api-key # CLAUDE_API_KEY also accepted diff --git a/AGENTS.md b/AGENTS.md index a3f5cd3..8f18d42 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ go test ./internal/domain/categorizer/... -v # single package Reads `config.yaml` or env vars: - `MONARCH_COOKIE` — required: a browser-copied Monarch session cookie (`sessionid=...; csrftoken=...`). The old `MONARCH_TOKEN` bearer token no longer works and is not read. - LLM categorizer — set **one** of: - - `OPENAI_API_KEY` (also `OPENAI_APIKEY`) with optional `OPENAI_MODEL` (default `gpt-5.6-luna`) + - `OPENAI_API_KEY` (also `OPENAI_APIKEY`) with optional `OPENAI_MODEL` (default `gpt-6-luna`) - `ANTHROPIC_API_KEY` (also `CLAUDE_API_KEY`) with optional `ANTHROPIC_MODEL` (default `claude-haiku-4-5`) - `CATEGORIZER_PROVIDER` — `openai` or `anthropic` to force a backend when both keys are set (auto-detected otherwise) - SQLite DB auto-created at `monarch_sync.db` diff --git a/README.md b/README.md index 6a993ed..7a4cfe4 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ monarch: openai: api_key: "${OPENAI_API_KEY}" - model: "gpt-5.6-luna" + model: "gpt-6-luna" anthropic: api_key: "${ANTHROPIC_API_KEY}" diff --git a/config.yaml b/config.yaml index a4d65be..4f7489c 100644 --- a/config.yaml +++ b/config.yaml @@ -36,7 +36,7 @@ monarch: # OpenAI configuration openai: api_key: "${OPENAI_API_KEY}" - model: "gpt-5.6-luna" + model: "gpt-6-luna" # Anthropic (Claude) configuration — used when CATEGORIZER_PROVIDER=anthropic # or when ANTHROPIC_API_KEY is the only LLM key set. diff --git a/docs/bug-fixes.md b/docs/bug-fixes.md index f97ae65..a2d47e3 100644 --- a/docs/bug-fixes.md +++ b/docs/bug-fixes.md @@ -12,6 +12,19 @@ Each bug fix entry should include: ## Bug Fixes +### 2026-09-23: GPT-6 models would be sent `temperature` instead of `reasoning_effort` + +**Description:** +The categorizer detected reasoning models with a `gpt-5` prefix check. Any `gpt-6-*` model (e.g. `OPENAI_MODEL=gpt-6-luna`) fell through to the non-reasoning path and was sent `temperature: 0.1` with no `reasoning_effort`. + +**Test Case:** +`TestIsReasoningModel` in `internal/domain/categorizer/categorizer_test.go` — the `gpt-6-luna`, `gpt-6-sol`, and ` GPT-6-Luna ` cases failed against the old prefix check. + +**Fix Applied:** +Replaced `isGPT5Model` with `isReasoningModel`, which parses the generation number after `gpt-` and treats generation 5 and later as reasoning models. The default OpenAI model moves to `gpt-6-luna` in the same change. + +**Commit:** Included in the pull request for this fix. + ### 2026-08-23: CI and release builds used a vulnerable Go patch release **Description:** diff --git a/internal/domain/categorizer/categorizer.go b/internal/domain/categorizer/categorizer.go index 7be7283..a9f5492 100644 --- a/internal/domain/categorizer/categorizer.go +++ b/internal/domain/categorizer/categorizer.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "strconv" "strings" "time" ) @@ -96,7 +97,7 @@ func NewCategorizer(client ChatClient, cache Cache, model string) *Categorizer { } } -const DefaultModel = "gpt-5.6-luna" +const DefaultModel = "gpt-6-luna" // CategorizeItems categorizes a list of items using available categories func (c *Categorizer) CategorizeItems(ctx context.Context, items []Item, categories []Category) (*CategorizationResult, error) { @@ -233,7 +234,7 @@ func (c *Categorizer) callLLM(ctx context.Context, items []Item, categories []Ca }, }, } - if isGPT5Model(c.Model) { + if isReasoningModel(c.Model) { reasoningEffort := "low" request.ReasoningEffort = &reasoningEffort } else { @@ -274,8 +275,19 @@ func (c *Categorizer) callLLM(ctx context.Context, items []Item, categories []Ca return nil, fmt.Errorf("%w after %d attempts", lastErr, maxRetries) } -func isGPT5Model(model string) bool { - return strings.HasPrefix(strings.ToLower(strings.TrimSpace(model)), "gpt-5") +// isReasoningModel reports whether model is a GPT-5-or-later reasoning model, +// which takes reasoning_effort and rejects a custom temperature. +func isReasoningModel(model string) bool { + rest, ok := strings.CutPrefix(strings.ToLower(strings.TrimSpace(model)), "gpt-") + if !ok { + return false + } + end := strings.IndexFunc(rest, func(r rune) bool { return r < '0' || r > '9' }) + if end == -1 { + end = len(rest) + } + generation, err := strconv.Atoi(rest[:end]) + return err == nil && generation >= 5 } // buildPrompt creates the prompt for OpenAI diff --git a/internal/domain/categorizer/categorizer_test.go b/internal/domain/categorizer/categorizer_test.go index bd6476f..6a724a1 100644 --- a/internal/domain/categorizer/categorizer_test.go +++ b/internal/domain/categorizer/categorizer_test.go @@ -425,3 +425,26 @@ func TestNewCategorizer_DefaultsModelWhenEmpty(t *testing.T) { assert.Equal(t, DefaultModel, categorizer.Model) } + +func TestIsReasoningModel(t *testing.T) { + tests := []struct { + model string + want bool + }{ + {"gpt-5.4-nano", true}, + {"gpt-5.6-luna", true}, + {"gpt-6-luna", true}, + {"gpt-6-sol", true}, + {" GPT-6-Luna ", true}, + {"gpt-4o-mini", false}, + {"gpt-4.1", false}, + {"claude-haiku-4-5", false}, + {"", false}, + } + + for _, tt := range tests { + t.Run(tt.model, func(t *testing.T) { + assert.Equal(t, tt.want, isReasoningModel(tt.model)) + }) + } +} diff --git a/internal/infrastructure/config/config.go b/internal/infrastructure/config/config.go index dd56d3d..cc4ee4b 100644 --- a/internal/infrastructure/config/config.go +++ b/internal/infrastructure/config/config.go @@ -188,7 +188,7 @@ func LoadFromEnv() *Config { }, OpenAI: OpenAIConfig{ APIKey: os.Getenv("OPENAI_API_KEY"), - Model: getEnv("OPENAI_MODEL", "gpt-5.6-luna"), + Model: getEnv("OPENAI_MODEL", "gpt-6-luna"), }, Anthropic: AnthropicConfig{ APIKey: firstNonEmpty(os.Getenv("ANTHROPIC_API_KEY"), os.Getenv("CLAUDE_API_KEY")), diff --git a/internal/infrastructure/config/config_test.go b/internal/infrastructure/config/config_test.go index e5729cb..52e3bc3 100644 --- a/internal/infrastructure/config/config_test.go +++ b/internal/infrastructure/config/config_test.go @@ -37,7 +37,7 @@ func TestLoadFromYAML(t *testing.T) { require.NoError(t, err) assert.NotNil(t, cfg) assert.Equal(t, "monarch_sync.db", cfg.Storage.DatabasePath) - assert.Equal(t, "gpt-5.6-luna", cfg.OpenAI.Model) + assert.Equal(t, "gpt-6-luna", cfg.OpenAI.Model) } func TestLoadFromEnv(t *testing.T) { @@ -66,7 +66,7 @@ func TestLoadFromEnv_Defaults(t *testing.T) { cfg := LoadFromEnv() assert.NotNil(t, cfg) assert.Equal(t, "monarch_sync.db", cfg.Storage.DatabasePath) - assert.Equal(t, "gpt-5.6-luna", cfg.OpenAI.Model) + assert.Equal(t, "gpt-6-luna", cfg.OpenAI.Model) } func TestLoadOrEnv(t *testing.T) {