Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
2 changes: 1 addition & 1 deletion config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions docs/bug-fixes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand Down
20 changes: 16 additions & 4 deletions internal/domain/categorizer/categorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions internal/domain/categorizer/categorizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
}
}
2 changes: 1 addition & 1 deletion internal/infrastructure/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down
4 changes: 2 additions & 2 deletions internal/infrastructure/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
Loading