diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a0bb245..dfb37920 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,12 @@ All notable changes to GoClaw are documented here. For full documentation, see [ with downgrade protection. WebSocket events `package.update.*` for owner clients. See `docs/packages-github.md` ยง "Updating Installed Packages". +### Changed + +- **ChatGPT Subscription (OAuth)** โ€” default model and backend-owned model catalog + now prefer `gpt-5.5`, with reasoning metadata and context-window defaults updated + for provider-first model selection. + ### Breaking Changes - **Context pruning now opt-in.** Previously tool-result trimming ran by default diff --git a/cmd/auth.go b/cmd/auth.go index 05091c5c..a591ff6a 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -45,7 +45,7 @@ func authStatusCmd() *cobra.Command { name = provider } fmt.Printf("ChatGPT OAuth account: active (alias: %s)\n", name) - fmt.Printf("Use model prefix '%s/' in agent config (e.g. %s/gpt-5.4).\n", name, name) + fmt.Printf("Use model prefix '%s/' in agent config (e.g. %s/gpt-5.5).\n", name, name) } else { fmt.Printf("No ChatGPT OAuth tokens found for alias '%s'.\n", provider) fmt.Println("Use the web UI to authenticate this ChatGPT OAuth account.") diff --git a/docs/02-providers.md b/docs/02-providers.md index df9893a8..b9633d0f 100644 --- a/docs/02-providers.md +++ b/docs/02-providers.md @@ -62,7 +62,7 @@ Streaming fallback is conservative: backup models are tried only if the stream f |----------|------|----------|---------------| | **anthropic** | Native HTTP + SSE | API key required | `claude-sonnet-4-5-20250929` | | **claude_cli** | stdio subprocess + MCP | Binary path (default: `claude`) | `sonnet` | -| **codex** | OAuth Responses API | OAuth token source | `gpt-5.3-codex` | +| **codex** | OAuth Responses API | OAuth token source | `gpt-5.5` | | **acp** | JSON-RPC 2.0 subagents | Binary + workspace dir | `claude` | | **dashscope** | OpenAI-compat wrapper | API key + custom models | `qwen3-max` | | **openai** (+ 10+ variants) | OpenAI-compatible | API key + endpoint URL | Model-specific | @@ -571,7 +571,7 @@ Claude CLI inherits thinking support from the underlying Claude model. Thinking ## 12. Codex Provider -The Codex provider integrates with OpenAI's ChatGPT Responses API (OAuth-based), enabling access to gpt-5.3-codex model through the chatgpt.com backend. Unlike standard OpenAI endpoints, Codex uses OAuth token refresh and a custom response format with "phase" markers. +The Codex provider integrates with OpenAI's ChatGPT Responses API (OAuth-based), defaulting to `gpt-5.5` through the chatgpt.com backend. Unlike standard OpenAI endpoints, Codex uses OAuth token refresh and a custom response format with "phase" markers. ### Configuration @@ -582,7 +582,7 @@ tokenSource := &MyTokenSource{} // implements TokenSource interface provider := NewCodexProvider("codex", tokenSource, "", "") // or specify custom API base and model: provider := NewCodexProvider("codex", tokenSource, - "https://chatgpt.com/backend-api", "gpt-5.3-codex") + "https://chatgpt.com/backend-api", "gpt-5.5") ``` ### API Endpoint @@ -601,7 +601,7 @@ Codex returns structured responses with phase markers: ```json { "id": "...", - "model": "gpt-5.3-codex", + "model": "gpt-5.5", "choices": [{ "message": { "role": "assistant", diff --git a/docs/18-http-api.md b/docs/18-http-api.md index 33047f4c..ca4aabea 100644 --- a/docs/18-http-api.md +++ b/docs/18-http-api.md @@ -148,7 +148,7 @@ can override or inherit routing behavior while keeping the main `provider` field ```json { "provider": "openai-codex", - "model": "gpt-5.4", + "model": "gpt-5.5", "chatgpt_oauth_routing": { "override_mode": "custom", "strategy": "round_robin" @@ -214,7 +214,7 @@ Agents can now store capability-aware GPT-5/Codex reasoning intent under top-lev ```json { "provider": "openai-codex", - "model": "gpt-5.4", + "model": "gpt-5.5", "reasoning_config": { "override_mode": "inherit" } @@ -364,11 +364,11 @@ Example response: { "models": [ { - "id": "gpt-5.4", - "name": "GPT-5.4", + "id": "gpt-5.5", + "name": "GPT-5.5", "reasoning": { "levels": ["none", "low", "medium", "high", "xhigh"], - "default_effort": "none" + "default_effort": "medium" } }, { diff --git a/docs/project-changelog.md b/docs/project-changelog.md index 014bf7e5..7f1ba5e9 100644 --- a/docs/project-changelog.md +++ b/docs/project-changelog.md @@ -6,6 +6,18 @@ Significant changes, features, and fixes in reverse chronological order. ## 2026-05-18 +### Providers: ChatGPT OAuth GPT-5.5 default + +**Changed** + +- Updated ChatGPT Subscription (OAuth) default model, provider model catalog, reasoning metadata, and docs examples to prefer `gpt-5.5`. + +**Tests** + +- Updated focused provider/model catalog, reasoning, registry, and token-window coverage for `gpt-5.5`. + +--- + ### Deployment: Codex CLI service-user auth **Fixes** diff --git a/internal/http/provider_models_catalog.go b/internal/http/provider_models_catalog.go index 57614795..60a252f9 100644 --- a/internal/http/provider_models_catalog.go +++ b/internal/http/provider_models_catalog.go @@ -1,5 +1,7 @@ package http +import "github.com/nextlevelbuilder/goclaw/internal/providers" + // bailianModels returns a hardcoded list of models available on the // Bailian Coding platform (coding-intl.dashscope.aliyuncs.com). // The platform does not expose a /v1/models endpoint. @@ -84,6 +86,7 @@ func acpModels() []ModelInfo { // chatGPTOAuthModels returns models available via ChatGPT OAuth integration. func chatGPTOAuthModels() []ModelInfo { return withReasoningCapabilities([]ModelInfo{ + {ID: providers.DefaultCodexModel, Name: "GPT-5.5"}, {ID: "gpt-5.4", Name: "GPT-5.4"}, {ID: "gpt-5.4-mini", Name: "GPT-5.4 Mini"}, {ID: "gpt-5.3-codex", Name: "GPT-5.3 Codex"}, diff --git a/internal/http/provider_models_test.go b/internal/http/provider_models_test.go index 265ae30f..c6f6a202 100644 --- a/internal/http/provider_models_test.go +++ b/internal/http/provider_models_test.go @@ -97,22 +97,22 @@ func TestProvidersHandlerListProviderModelsChatGPTOAuthIncludesReasoningMetadata var found bool for _, model := range result.Models { - if model.ID != "gpt-5.4" { + if model.ID != "gpt-5.5" { continue } found = true if model.Reasoning == nil { - t.Fatal("gpt-5.4 reasoning = nil, want capability metadata") + t.Fatal("gpt-5.5 reasoning = nil, want capability metadata") } - if model.Reasoning.DefaultEffort != "none" { - t.Fatalf("gpt-5.4 default_effort = %q, want none", model.Reasoning.DefaultEffort) + if model.Reasoning.DefaultEffort != "medium" { + t.Fatalf("gpt-5.5 default_effort = %q, want medium", model.Reasoning.DefaultEffort) } if got := model.Reasoning.Levels; len(got) != 5 || got[4] != "xhigh" { - t.Fatalf("gpt-5.4 levels = %#v, want none..xhigh", got) + t.Fatalf("gpt-5.5 levels = %#v, want none..xhigh", got) } } if !found { - t.Fatal("gpt-5.4 not found in ChatGPT OAuth model list") + t.Fatal("gpt-5.5 not found in ChatGPT OAuth model list") } } diff --git a/internal/providers/adapter_codex.go b/internal/providers/adapter_codex.go index 8f3f856b..42aaf691 100644 --- a/internal/providers/adapter_codex.go +++ b/internal/providers/adapter_codex.go @@ -26,7 +26,7 @@ func NewCodexAdapter(cfg ProviderConfig) (ProviderAdapter, error) { model := cfg.Model if model == "" { - model = "gpt-5.4" + model = DefaultCodexModel } var ts TokenSource @@ -53,7 +53,7 @@ func (a *CodexAdapter) Capabilities() ProviderCapabilities { Vision: true, CacheControl: false, ImageGeneration: true, // Codex (OpenAI Responses API) supports native image_generation tool - MaxContextWindow: 1_000_000, + MaxContextWindow: 1_050_000, TokenizerID: "o200k_base", } } diff --git a/internal/providers/adapter_codex_test.go b/internal/providers/adapter_codex_test.go index fe2967c6..44eca806 100644 --- a/internal/providers/adapter_codex_test.go +++ b/internal/providers/adapter_codex_test.go @@ -35,12 +35,23 @@ func TestCodexAdapter_Defaults(t *testing.T) { if !caps.Thinking || !caps.Vision { t.Errorf("expected Thinking+Vision, got %+v", caps) } - if caps.MaxContextWindow != 1_000_000 { - t.Errorf("MaxContextWindow = %d, want 1_000_000", caps.MaxContextWindow) + if caps.MaxContextWindow != 1_050_000 { + t.Errorf("MaxContextWindow = %d, want 1_050_000", caps.MaxContextWindow) } if caps.TokenizerID != "o200k_base" { t.Errorf("TokenizerID = %q, want o200k_base", caps.TokenizerID) } + body, _, err := a.ToRequest(ChatRequest{Messages: []Message{{Role: "user", Content: "hi"}}}) + if err != nil { + t.Fatalf("ToRequest error: %v", err) + } + var payload map[string]any + if err := json.Unmarshal(body, &payload); err != nil { + t.Fatalf("decode body: %v", err) + } + if payload["model"] != DefaultCodexModel { + t.Errorf("default model = %v, want %s", payload["model"], DefaultCodexModel) + } } // TestCodexAdapter_BaseURLTrimmed verifies trailing slashes are stripped from diff --git a/internal/providers/codex.go b/internal/providers/codex.go index c5626093..e876ff20 100644 --- a/internal/providers/codex.go +++ b/internal/providers/codex.go @@ -15,6 +15,9 @@ type CodexRoutingDefaults struct { ExtraProviderNames []string } +// DefaultCodexModel is the default model for ChatGPT Subscription (OAuth). +const DefaultCodexModel = "gpt-5.5" + // CodexProvider implements Provider for the OpenAI Responses API, // used with ChatGPT subscription via OAuth (Codex flow). // Wire format: POST /codex/responses on chatgpt.com backend. @@ -37,7 +40,7 @@ func NewCodexProvider(name string, tokenSource TokenSource, apiBase, defaultMode apiBase = strings.TrimRight(apiBase, "/") if defaultModel == "" { - defaultModel = "gpt-5.4" + defaultModel = DefaultCodexModel } return &CodexProvider{ @@ -78,7 +81,7 @@ func (p *CodexProvider) Capabilities() ProviderCapabilities { Vision: true, CacheControl: false, ImageGeneration: true, // Codex (OpenAI Responses API) supports native image_generation tool - MaxContextWindow: 1_000_000, + MaxContextWindow: 1_050_000, TokenizerID: "o200k_base", } } diff --git a/internal/providers/codex_test.go b/internal/providers/codex_test.go index 0326a0fb..b59a5ad9 100644 --- a/internal/providers/codex_test.go +++ b/internal/providers/codex_test.go @@ -47,8 +47,8 @@ func TestCodexProviderName(t *testing.T) { func TestCodexProviderDefaultModel(t *testing.T) { p := NewCodexProvider("test", &staticTokenSource{token: "test"}, "", "") - if p.DefaultModel() != "gpt-5.4" { - t.Errorf("DefaultModel() = %q, want %q", p.DefaultModel(), "gpt-5.4") + if p.DefaultModel() != DefaultCodexModel { + t.Errorf("DefaultModel() = %q, want %q", p.DefaultModel(), DefaultCodexModel) } p2 := NewCodexProvider("test", &staticTokenSource{token: "test"}, "", "o3") diff --git a/internal/providers/forward_compat_openai.go b/internal/providers/forward_compat_openai.go index 86984d4e..a90e6fa4 100644 --- a/internal/providers/forward_compat_openai.go +++ b/internal/providers/forward_compat_openai.go @@ -11,11 +11,10 @@ var openAIForwardCompatMap = map[string]struct { Patch *ModelSpec }{ "gpt-5.5": { - Template: "gpt-5.4", - Patch: &ModelSpec{ContextWindow: 1_000_000, MaxTokens: 200_000}, + Template: "gpt-5.5", }, "gpt-5.6": { - Template: "gpt-5.4", + Template: "gpt-5.5", Patch: &ModelSpec{ContextWindow: 2_000_000, MaxTokens: 200_000}, }, "o5-mini": { @@ -24,7 +23,7 @@ var openAIForwardCompatMap = map[string]struct { }, } -// ResolveForwardCompat handles models like "gpt-5.5" by cloning from "gpt-5.4". +// ResolveForwardCompat handles future model aliases by cloning from the latest known template. func (r *OpenAIForwardCompat) ResolveForwardCompat(modelID string, registry ModelRegistry) *ModelSpec { // Direct map lookup (exact match) if entry, ok := openAIForwardCompatMap[modelID]; ok { diff --git a/internal/providers/model_registry.go b/internal/providers/model_registry.go index 9cc9be93..113c8f31 100644 --- a/internal/providers/model_registry.go +++ b/internal/providers/model_registry.go @@ -154,6 +154,7 @@ func SeedDefaultModels(r *InMemoryRegistry) { // OpenAI models for _, s := range []ModelSpec{ + {ID: "gpt-5.5", Provider: "openai", ContextWindow: 1_050_000, MaxTokens: 128_000, Reasoning: true, Vision: true, TokenizerID: "o200k_base"}, {ID: "gpt-5.4", Provider: "openai", ContextWindow: 1_000_000, MaxTokens: 100_000, Reasoning: true, Vision: true, TokenizerID: "o200k_base"}, {ID: "gpt-5.2", Provider: "openai", ContextWindow: 256_000, MaxTokens: 64_000, Reasoning: true, Vision: true, TokenizerID: "o200k_base"}, {ID: "gpt-4o", Provider: "openai", ContextWindow: 128_000, MaxTokens: 16_384, Reasoning: false, Vision: true, TokenizerID: "o200k_base"}, diff --git a/internal/providers/model_registry_test.go b/internal/providers/model_registry_test.go index 2da4d831..abf6cf19 100644 --- a/internal/providers/model_registry_test.go +++ b/internal/providers/model_registry_test.go @@ -263,10 +263,10 @@ func TestCloneFromTemplatePatchBooleanFields(t *testing.T) { registry := &InMemoryRegistry{} template := ModelSpec{ - ID: "base", - Provider: "openai", + ID: "base", + Provider: "openai", Reasoning: false, - Vision: false, + Vision: false, } registry.Register(template) @@ -371,7 +371,7 @@ func TestOpenAIForwardCompatResolveExactMatch(t *testing.T) { resolver := &OpenAIForwardCompat{} registry.RegisterResolver("openai", resolver) - // Try to resolve gpt-5.5 which should use gpt-5.4 as template with patch + // gpt-5.5 is a seeded model, so direct resolution should win. resolved := registry.Resolve("openai", "gpt-5.5") if resolved == nil { @@ -380,12 +380,11 @@ func TestOpenAIForwardCompatResolveExactMatch(t *testing.T) { if resolved.ID != "gpt-5.5" { t.Errorf("expected ID=gpt-5.5, got %s", resolved.ID) } - // Should have patched values from the map - if resolved.ContextWindow != 1_000_000 { - t.Errorf("expected ContextWindow=1000000 from patch, got %d", resolved.ContextWindow) + if resolved.ContextWindow != 1_050_000 { + t.Errorf("expected ContextWindow=1050000, got %d", resolved.ContextWindow) } - if resolved.MaxTokens != 200_000 { - t.Errorf("expected MaxTokens=200000 from patch, got %d", resolved.MaxTokens) + if resolved.MaxTokens != 128_000 { + t.Errorf("expected MaxTokens=128000, got %d", resolved.MaxTokens) } } @@ -403,9 +402,12 @@ func TestOpenAIForwardCompatResolvePrefixMatch(t *testing.T) { if resolved.ID != "gpt-5.5-turbo" { t.Errorf("expected ID=gpt-5.5-turbo, got %s", resolved.ID) } - // Should use gpt-5.4 as template with gpt-5.5 patch - if resolved.MaxTokens != 200_000 { - t.Errorf("expected MaxTokens=200000 from patch, got %d", resolved.MaxTokens) + // Should use gpt-5.5 as the latest known template. + if resolved.ContextWindow != 1_050_000 { + t.Errorf("expected ContextWindow=1050000 from template, got %d", resolved.ContextWindow) + } + if resolved.MaxTokens != 128_000 { + t.Errorf("expected MaxTokens=128000 from template, got %d", resolved.MaxTokens) } } @@ -462,8 +464,8 @@ func TestCloneFromTemplatePatchZeroValuesIgnored(t *testing.T) { // Patch with zero values should be ignored patch := &ModelSpec{ - ContextWindow: 0, // Should be ignored - MaxTokens: 0, // Should be ignored + ContextWindow: 0, // Should be ignored + MaxTokens: 0, // Should be ignored TokenizerID: "", // Should be ignored Cost: ModelCost{ InputPer1M: 0, // Should be ignored diff --git a/internal/providers/native_image.go b/internal/providers/native_image.go index d836dfa3..1604c616 100644 --- a/internal/providers/native_image.go +++ b/internal/providers/native_image.go @@ -41,7 +41,7 @@ func ValidateImageModel(model string) (string, error) { // NativeImageRequest describes a single image generation request. type NativeImageRequest struct { - // Model is the parent LLM model for the Responses API call (e.g. "gpt-5.4"). + // Model is the parent LLM model for the Responses API call (e.g. "gpt-5.5"). // NOT the image model โ€” see ImageModel below. // If empty, the provider uses its own default LLM model. Model string diff --git a/internal/providers/openai_request.go b/internal/providers/openai_request.go index 24e7f2c8..39377034 100644 --- a/internal/providers/openai_request.go +++ b/internal/providers/openai_request.go @@ -181,7 +181,7 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream if v, ok := req.Options[OptTemperature]; ok { // Certain model families don't support custom temperature (locked to default). // This is a model-level constraint, not provider-specific โ€” applies to both OpenAI and Azure. - // Note: gpt-5.X flagship models (gpt-5.1, gpt-5.4) DO support temperature; + // Note: gpt-5.X flagship models (gpt-5.1, gpt-5.4, gpt-5.5) DO support temperature; // only the mini/nano reasoning variants reject it. skipTemp := strings.HasPrefix(capabilityModel, "gpt-5-mini") || strings.HasPrefix(capabilityModel, "gpt-5-nano") || strings.HasPrefix(capabilityModel, "o1") || strings.HasPrefix(capabilityModel, "o3") || strings.HasPrefix(capabilityModel, "o4") if !skipTemp { diff --git a/internal/providers/reasoning_capability.go b/internal/providers/reasoning_capability.go index c2c3728c..e75c9223 100644 --- a/internal/providers/reasoning_capability.go +++ b/internal/providers/reasoning_capability.go @@ -25,6 +25,7 @@ type reasoningCapabilityEntry struct { } var reasoningCapabilityEntries = []reasoningCapabilityEntry{ + {id: "gpt-5.5", capability: ReasoningCapability{Levels: []string{"none", "low", "medium", "high", "xhigh"}, DefaultEffort: "medium"}}, {id: "gpt-5.4-mini", capability: ReasoningCapability{Levels: []string{"none", "low", "medium", "high", "xhigh"}, DefaultEffort: "none"}}, {id: "gpt-5-mini", capability: ReasoningCapability{Levels: []string{"none", "low", "medium", "high", "xhigh"}, DefaultEffort: "none"}}, {id: "gpt-5.4", capability: ReasoningCapability{Levels: []string{"none", "low", "medium", "high", "xhigh"}, DefaultEffort: "none"}}, diff --git a/internal/providers/reasoning_resolution_test.go b/internal/providers/reasoning_resolution_test.go index b5338ae3..824afba4 100644 --- a/internal/providers/reasoning_resolution_test.go +++ b/internal/providers/reasoning_resolution_test.go @@ -30,6 +30,14 @@ func TestLookupReasoningCapability(t *testing.T) { if capability.Supports("low") { t.Fatal("expected gpt-5.1-codex-max to reject low") } + + capability = LookupReasoningCapability("gpt-5.5") + if capability == nil { + t.Fatal("LookupReasoningCapability(gpt-5.5) = nil, want capability") + } + if capability.DefaultEffort != "medium" { + t.Fatalf("gpt-5.5 default_effort = %q, want medium", capability.DefaultEffort) + } } func TestResolveReasoningDecisionDowngradesUnsupportedEffort(t *testing.T) { diff --git a/internal/tokencount/tiktoken_counter_test.go b/internal/tokencount/tiktoken_counter_test.go index 1767ed0a..85bb00f3 100644 --- a/internal/tokencount/tiktoken_counter_test.go +++ b/internal/tokencount/tiktoken_counter_test.go @@ -82,6 +82,7 @@ func TestModelContextWindow(t *testing.T) { }{ {"claude-sonnet-4-5-20250929", 200_000}, {"gpt-4o-mini", 128_000}, + {"gpt-5.5", 1_050_000}, {"gpt-5.4", 1_000_000}, {"unknown-model", 200_000}, // conservative default } diff --git a/internal/tokencount/token_counter.go b/internal/tokencount/token_counter.go index ecdc73ca..9a237283 100644 --- a/internal/tokencount/token_counter.go +++ b/internal/tokencount/token_counter.go @@ -31,8 +31,8 @@ type TokenizerID string const ( TokenizerCL100K TokenizerID = "cl100k_base" // Claude, GPT-3.5/4 - TokenizerO200K TokenizerID = "o200k_base" // GPT-4o, GPT-5 - TokenizerFallback TokenizerID = "fallback" // rune-count / 3 + TokenizerO200K TokenizerID = "o200k_base" // GPT-4o, GPT-5 + TokenizerFallback TokenizerID = "fallback" // rune-count / 3 ) // ModelInfo maps a model name prefix to its tokenizer + context window. @@ -47,6 +47,7 @@ var DefaultRegistry = map[string]ModelInfo{ "claude-": {TokenizerCL100K, 200_000}, "gpt-4o": {TokenizerO200K, 128_000}, "gpt-4": {TokenizerCL100K, 128_000}, + "gpt-5.5": {TokenizerO200K, 1_050_000}, "gpt-5": {TokenizerO200K, 1_000_000}, "qwen-": {TokenizerCL100K, 128_000}, "deepseek-": {TokenizerCL100K, 128_000}, diff --git a/ui/web/src/pages/providers/provider-oauth-section.tsx b/ui/web/src/pages/providers/provider-oauth-section.tsx index 60de4942..e357ff70 100644 --- a/ui/web/src/pages/providers/provider-oauth-section.tsx +++ b/ui/web/src/pages/providers/provider-oauth-section.tsx @@ -61,7 +61,7 @@ export function OAuthSection({

{t("oauth.modelPrefixHint")} {provider}/{" "} {t("oauth.modelPrefixExample", { - example: `${provider}/gpt-5.4`, + example: `${provider}/gpt-5.5`, })}

@@ -244,7 +244,7 @@ export function OAuthSection({

{t("oauth.modelPrefixHint")} {resolvedProviderName}/{" "} {t("oauth.modelPrefixExample", { - example: `${resolvedProviderName}/gpt-5.4`, + example: `${resolvedProviderName}/gpt-5.5`, })}