feat: add Opus 5.5 support for Anthropic and Copilot
- commit
- e967eb8b76458225d882fd86033bd22bd7f41335
- author
- patriceckhart · · 2026-09-23 05:42 UTC
- parent
- 81f384a
- signature
- unsigned
- refs
- tag: v0.3.87
10 files · +106 -14
M README.md +3 -1
| @@ -748,7 +748,9 @@ Use `/login` and pick **api key** to paste an AI Studio key. zot probes `/v1beta | |||
| 748 | 748 | ||
| 749 | 749 | > **Free-tier rate limits.** AI Studio's free tier has tight per-minute and per-day caps that vary by model: `gemini-2.5-pro` is the strictest (a few requests per minute, ~50 per day), Flash and Flash-Lite are far more generous. If a Pro turn 429s with `"You exceeded your current quota"` while Flash on the same key still works, you've hit the Pro free-tier RPD. Either switch to Flash for agent loops, or [enable billing](https://aistudio.google.com/app/apikey) on your AI Studio project to flip the same key from free to pay-as-you-go pricing (`$1.25/M` input, `$10/M` output for Pro). | |
| 750 | 750 | ||
| 751 | - | Reasoning levels (`--reasoning off|minimum|low|medium|high|xhigh|max`, also configurable through `/reasoning` or in `/settings` as **reasoning level**) map differently per generation. `max` is a distinct opt-in tier above `xhigh`. GPT-5.6 and adaptive-thinking Claude models receive native `max`; unsupported providers clamp it to their highest accepted effort. Budget-based providers retain their provider/model caps. Gemini 3.x uses the `thinkingLevel` enum (`MINIMAL`/`LOW`/`MEDIUM`/`HIGH`), with Gemini-3-Pro pinned to `LOW` minimum and `HIGH` for any medium-or-higher request. `off` sends no reasoning config. Gemini 2.0 models have no thinking config. | |
| 751 | + | Reasoning levels (`--reasoning off|minimum|low|medium|high|xhigh|max`, also configurable through `/reasoning` or in `/settings` as **reasoning level**) map differently per generation. `max` is a distinct opt-in tier above `xhigh`. GPT-5.6 and adaptive-thinking Claude models receive native `max`; unsupported providers clamp it to their highest accepted effort. Budget-based providers retain their provider/model caps. Gemini 3.x uses the `thinkingLevel` enum (`MINIMAL`/`LOW`/`MEDIUM`/`HIGH`), with Gemini-3-Pro pinned to `LOW` minimum and `HIGH` for any medium-or-higher request. `off` sends no reasoning config except on always-on thinking models. Gemini 2.0 models have no thinking config. | |
| 752 | + | ||
| 753 | + | Claude Opus 5.5 is available through Anthropic (`--provider anthropic --model claude-opus-5-5`) and GitHub Copilot (`--provider github-copilot --model claude-opus-5.5`, subject to account availability). Both entries support a 1,000,000-token context window and up to 128,000 output tokens. Adaptive thinking is always on, with `low`, `medium`, `high`, `xhigh`, and `max` effort levels. The default, `off`, and `minimum` requests use `low` instead. The reasoning selector omits unsupported levels. Catalog prices per million tokens are $4 input, $20 output, $0.20 cache reads, and $5 cache writes. Copilot billing remains governed by your plan. | |
| 752 | 754 | ||
| 753 | 755 | You can add additional Gemini model IDs to `models.json` under the `google` provider. | |
| 754 | 756 | ||
| @@ -748,7 +748,9 @@ Use `/login` and pick **api key** to paste an AI Studio key. zot probes `/v1beta | |||||
| 748 | 748 | ||||
| 749 | > **Free-tier rate limits.** AI Studio's free tier has tight per-minute and per-day caps that vary by model: `gemini-2.5-pro` is the strictest (a few requests per minute, ~50 per day), Flash and Flash-Lite are far more generous. If a Pro turn 429s with `"You exceeded your current quota"` while Flash on the same key still works, you've hit the Pro free-tier RPD. Either switch to Flash for agent loops, or [enable billing](https://aistudio.google.com/app/apikey) on your AI Studio project to flip the same key from free to pay-as-you-go pricing (`$1.25/M` input, `$10/M` output for Pro). | 749 | > **Free-tier rate limits.** AI Studio's free tier has tight per-minute and per-day caps that vary by model: `gemini-2.5-pro` is the strictest (a few requests per minute, ~50 per day), Flash and Flash-Lite are far more generous. If a Pro turn 429s with `"You exceeded your current quota"` while Flash on the same key still works, you've hit the Pro free-tier RPD. Either switch to Flash for agent loops, or [enable billing](https://aistudio.google.com/app/apikey) on your AI Studio project to flip the same key from free to pay-as-you-go pricing (`$1.25/M` input, `$10/M` output for Pro). | ||
| 750 | 750 | ||||
| 751 | - | Reasoning levels (`--reasoning off|minimum|low|medium|high|xhigh|max`, also configurable through `/reasoning` or in `/settings` as **reasoning level**) map differently per generation. `max` is a distinct opt-in tier above `xhigh`. GPT-5.6 and adaptive-thinking Claude models receive native `max`; unsupported providers clamp it to their highest accepted effort. Budget-based providers retain their provider/model caps. Gemini 3.x uses the `thinkingLevel` enum (`MINIMAL`/`LOW`/`MEDIUM`/`HIGH`), with Gemini-3-Pro pinned to `LOW` minimum and `HIGH` for any medium-or-higher request. `off` sends no reasoning config. Gemini 2.0 models have no thinking config. | 751 | + | Reasoning levels (`--reasoning off|minimum|low|medium|high|xhigh|max`, also configurable through `/reasoning` or in `/settings` as **reasoning level**) map differently per generation. `max` is a distinct opt-in tier above `xhigh`. GPT-5.6 and adaptive-thinking Claude models receive native `max`; unsupported providers clamp it to their highest accepted effort. Budget-based providers retain their provider/model caps. Gemini 3.x uses the `thinkingLevel` enum (`MINIMAL`/`LOW`/`MEDIUM`/`HIGH`), with Gemini-3-Pro pinned to `LOW` minimum and `HIGH` for any medium-or-higher request. `off` sends no reasoning config except on always-on thinking models. Gemini 2.0 models have no thinking config. |
| 752 | + | ||||
| 753 | + | Claude Opus 5.5 is available through Anthropic (`--provider anthropic --model claude-opus-5-5`) and GitHub Copilot (`--provider github-copilot --model claude-opus-5.5`, subject to account availability). Both entries support a 1,000,000-token context window and up to 128,000 output tokens. Adaptive thinking is always on, with `low`, `medium`, `high`, `xhigh`, and `max` effort levels. The default, `off`, and `minimum` requests use `low` instead. The reasoning selector omits unsupported levels. Catalog prices per million tokens are $4 input, $20 output, $0.20 cache reads, and $5 cache writes. Copilot billing remains governed by your plan. | |||
| 752 | 754 | ||||
| 753 | You can add additional Gemini model IDs to `models.json` under the `google` provider. | 755 | You can add additional Gemini model IDs to `models.json` under the `google` provider. | ||
| 754 | 756 | ||||
M packages/provider/anthropic.go +1 -1
| @@ -21,7 +21,7 @@ const anthropicAPIVersion = "2023-06-01" | |||
| 21 | 21 | // accepts the request; diverging from them causes 429 rate_limit_error | |
| 22 | 22 | // or 403 on the very first request. | |
| 23 | 23 | const ( | |
| 24 | - | claudeCodeVersion = "2.1.258" | |
| 24 | + | claudeCodeVersion = "2.1.280" | |
| 25 | 25 | claudeCodeIdentity = "You are Claude Code, Anthropic's official CLI for Claude." | |
| 26 | 26 | ) | |
| 27 | 27 | ||
| @@ -21,7 +21,7 @@ const anthropicAPIVersion = "2023-06-01" | |||||
| 21 | // accepts the request; diverging from them causes 429 rate_limit_error | 21 | // accepts the request; diverging from them causes 429 rate_limit_error | ||
| 22 | // or 403 on the very first request. | 22 | // or 403 on the very first request. | ||
| 23 | const ( | 23 | const ( | ||
| 24 | - | claudeCodeVersion = "2.1.258" | 24 | + | claudeCodeVersion = "2.1.280" |
| 25 | claudeCodeIdentity = "You are Claude Code, Anthropic's official CLI for Claude." | 25 | claudeCodeIdentity = "You are Claude Code, Anthropic's official CLI for Claude." | ||
| 26 | ) | 26 | ) | ||
| 27 | 27 | ||||
M packages/provider/catalog_builtin.go +1 -0
| @@ -153,6 +153,7 @@ var builtinCatalog = []Model{ | |||
| 153 | 153 | {Provider: "github-copilot", ID: "claude-fable-5.1", DisplayName: "Claude Fable 5.1", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 10, PriceOutput: 50, PriceCacheRead: 0.25, PriceCacheWrite: 12.5, BaseURL: "https://api.individual.githubcopilot.com"}, | |
| 154 | 154 | {Provider: "github-copilot", ID: "claude-opus-4.8-fast", DisplayName: "Claude Opus 4.8 (fast mode)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, AdaptiveThinking: true, PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5, BaseURL: "https://api.individual.githubcopilot.com"}, | |
| 155 | 155 | {Provider: "github-copilot", ID: "claude-opus-5", DisplayName: "Claude Opus 5", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, AdaptiveThinking: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://api.individual.githubcopilot.com"}, | |
| 156 | + | {Provider: "github-copilot", ID: "claude-opus-5.5", DisplayName: "Claude Opus 5.5", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://api.individual.githubcopilot.com"}, | |
| 156 | 157 | {Provider: "github-copilot", ID: "gemini-3.6-flash", DisplayName: "Gemini 3.6 Flash", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 0.75, PriceOutput: 3.75, PriceCacheRead: 0.075, BaseURL: "https://api.individual.githubcopilot.com"}, | |
| 157 | 158 | {Provider: "github-copilot", ID: "gemini-3.7-flash", DisplayName: "Gemini 3.7 Flash", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 0.75, PriceOutput: 3.75, PriceCacheRead: 0.075, BaseURL: "https://api.individual.githubcopilot.com"}, | |
| 158 | 159 | {Provider: "github-copilot", ID: "gemini-3.8-flash", DisplayName: "Gemini 3.8 Flash", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 0.75, PriceOutput: 3.75, PriceCacheRead: 0.075, BaseURL: "https://api.individual.githubcopilot.com"}, | |
| @@ -153,6 +153,7 @@ var builtinCatalog = []Model{ | |||||
| 153 | {Provider: "github-copilot", ID: "claude-fable-5.1", DisplayName: "Claude Fable 5.1", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 10, PriceOutput: 50, PriceCacheRead: 0.25, PriceCacheWrite: 12.5, BaseURL: "https://api.individual.githubcopilot.com"}, | 153 | {Provider: "github-copilot", ID: "claude-fable-5.1", DisplayName: "Claude Fable 5.1", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 10, PriceOutput: 50, PriceCacheRead: 0.25, PriceCacheWrite: 12.5, BaseURL: "https://api.individual.githubcopilot.com"}, | ||
| 154 | {Provider: "github-copilot", ID: "claude-opus-4.8-fast", DisplayName: "Claude Opus 4.8 (fast mode)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, AdaptiveThinking: true, PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5, BaseURL: "https://api.individual.githubcopilot.com"}, | 154 | {Provider: "github-copilot", ID: "claude-opus-4.8-fast", DisplayName: "Claude Opus 4.8 (fast mode)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, AdaptiveThinking: true, PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5, BaseURL: "https://api.individual.githubcopilot.com"}, | ||
| 155 | {Provider: "github-copilot", ID: "claude-opus-5", DisplayName: "Claude Opus 5", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, AdaptiveThinking: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://api.individual.githubcopilot.com"}, | 155 | {Provider: "github-copilot", ID: "claude-opus-5", DisplayName: "Claude Opus 5", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, AdaptiveThinking: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://api.individual.githubcopilot.com"}, | ||
| 156 | + | {Provider: "github-copilot", ID: "claude-opus-5.5", DisplayName: "Claude Opus 5.5", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://api.individual.githubcopilot.com"}, | |||
| 156 | {Provider: "github-copilot", ID: "gemini-3.6-flash", DisplayName: "Gemini 3.6 Flash", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 0.75, PriceOutput: 3.75, PriceCacheRead: 0.075, BaseURL: "https://api.individual.githubcopilot.com"}, | 157 | {Provider: "github-copilot", ID: "gemini-3.6-flash", DisplayName: "Gemini 3.6 Flash", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 0.75, PriceOutput: 3.75, PriceCacheRead: 0.075, BaseURL: "https://api.individual.githubcopilot.com"}, | ||
| 157 | {Provider: "github-copilot", ID: "gemini-3.7-flash", DisplayName: "Gemini 3.7 Flash", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 0.75, PriceOutput: 3.75, PriceCacheRead: 0.075, BaseURL: "https://api.individual.githubcopilot.com"}, | 158 | {Provider: "github-copilot", ID: "gemini-3.7-flash", DisplayName: "Gemini 3.7 Flash", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 0.75, PriceOutput: 3.75, PriceCacheRead: 0.075, BaseURL: "https://api.individual.githubcopilot.com"}, | ||
| 158 | {Provider: "github-copilot", ID: "gemini-3.8-flash", DisplayName: "Gemini 3.8 Flash", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 0.75, PriceOutput: 3.75, PriceCacheRead: 0.075, BaseURL: "https://api.individual.githubcopilot.com"}, | 159 | {Provider: "github-copilot", ID: "gemini-3.8-flash", DisplayName: "Gemini 3.8 Flash", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 0.75, PriceOutput: 3.75, PriceCacheRead: 0.075, BaseURL: "https://api.individual.githubcopilot.com"}, | ||
M packages/provider/copilot_catalog_test.go +1 -1
| @@ -7,7 +7,7 @@ import ( | |||
| 7 | 7 | ||
| 8 | 8 | func TestCopilotCatalogCoversPublicChatModels(t *testing.T) { | |
| 9 | 9 | groups := map[string][]string{ | |
| 10 | - | APIAnthropicMessages: {"claude-fable-5", "claude-fable-5.1", "claude-haiku-4.5", "claude-opus-4.7", "claude-opus-4.8", "claude-opus-4.8-fast", "claude-opus-5", "claude-sonnet-5"}, | |
| 10 | + | APIAnthropicMessages: {"claude-fable-5", "claude-fable-5.1", "claude-haiku-4.5", "claude-opus-4.7", "claude-opus-4.8", "claude-opus-4.8-fast", "claude-opus-5", "claude-opus-5.5", "claude-sonnet-5"}, | |
| 11 | 11 | APIResponses: {"gpt-5-mini", "gpt-5.3-codex", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-6-astra", "grok-4.5", "grok-4.6", "mai-code-1-flash-picker", "mai-code-1.1-flash"}, | |
| 12 | 12 | APICompletions: {"gemini-3.5-flash", "gemini-3.6-flash", "gemini-3.7-flash", "gemini-3.8-flash", "kimi-k2.7-code", "kimi-k3"}, | |
| 13 | 13 | } | |
| @@ -7,7 +7,7 @@ import ( | |||||
| 7 | 7 | ||||
| 8 | func TestCopilotCatalogCoversPublicChatModels(t *testing.T) { | 8 | func TestCopilotCatalogCoversPublicChatModels(t *testing.T) { | ||
| 9 | groups := map[string][]string{ | 9 | groups := map[string][]string{ | ||
| 10 | - | APIAnthropicMessages: {"claude-fable-5", "claude-fable-5.1", "claude-haiku-4.5", "claude-opus-4.7", "claude-opus-4.8", "claude-opus-4.8-fast", "claude-opus-5", "claude-sonnet-5"}, | 10 | + | APIAnthropicMessages: {"claude-fable-5", "claude-fable-5.1", "claude-haiku-4.5", "claude-opus-4.7", "claude-opus-4.8", "claude-opus-4.8-fast", "claude-opus-5", "claude-opus-5.5", "claude-sonnet-5"}, |
| 11 | APIResponses: {"gpt-5-mini", "gpt-5.3-codex", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-6-astra", "grok-4.5", "grok-4.6", "mai-code-1-flash-picker", "mai-code-1.1-flash"}, | 11 | APIResponses: {"gpt-5-mini", "gpt-5.3-codex", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-6-astra", "grok-4.5", "grok-4.6", "mai-code-1-flash-picker", "mai-code-1.1-flash"}, | ||
| 12 | APICompletions: {"gemini-3.5-flash", "gemini-3.6-flash", "gemini-3.7-flash", "gemini-3.8-flash", "kimi-k2.7-code", "kimi-k3"}, | 12 | APICompletions: {"gemini-3.5-flash", "gemini-3.6-flash", "gemini-3.7-flash", "gemini-3.8-flash", "kimi-k2.7-code", "kimi-k3"}, | ||
| 13 | } | 13 | } | ||
M packages/provider/discover_copilot_test.go +1 -1
| @@ -97,7 +97,7 @@ func TestModelAvailabilityFiltersWithoutDeletingCatalog(t *testing.T) { | |||
| 97 | 97 | t.Error("empty availability did not hide models") | |
| 98 | 98 | } | |
| 99 | 99 | SetModelAvailability("github-copilot", nil) | |
| 100 | - | if len(ModelsForProvider("github-copilot")) != 27 { | |
| 100 | + | if len(ModelsForProvider("github-copilot")) != 28 { | |
| 101 | 101 | t.Error("clearing restriction did not restore catalog") | |
| 102 | 102 | } | |
| 103 | 103 | } | |
| @@ -97,7 +97,7 @@ func TestModelAvailabilityFiltersWithoutDeletingCatalog(t *testing.T) { | |||||
| 97 | t.Error("empty availability did not hide models") | 97 | t.Error("empty availability did not hide models") | ||
| 98 | } | 98 | } | ||
| 99 | SetModelAvailability("github-copilot", nil) | 99 | SetModelAvailability("github-copilot", nil) | ||
| 100 | - | if len(ModelsForProvider("github-copilot")) != 27 { | 100 | + | if len(ModelsForProvider("github-copilot")) != 28 { |
| 101 | t.Error("clearing restriction did not restore catalog") | 101 | t.Error("clearing restriction did not restore catalog") | ||
| 102 | } | 102 | } | ||
| 103 | } | 103 | } | ||
M packages/provider/github_copilot_wire_test.go +2 -1
| @@ -28,6 +28,7 @@ func seedCopilotTestToken(t *testing.T, baseURL string) string { | |||
| 28 | 28 | func TestCopilotWireProtocols(t *testing.T) { | |
| 29 | 29 | for _, tc := range []struct{ model, path string }{ | |
| 30 | 30 | {"claude-sonnet-5", "/v1/messages"}, | |
| 31 | + | {"claude-opus-5.5", "/v1/messages"}, | |
| 31 | 32 | {"claude-haiku-4.5", "/v1/messages"}, | |
| 32 | 33 | {"gpt-5.4", "/responses"}, | |
| 33 | 34 | {"grok-4.6", "/responses"}, | |
| @@ -69,7 +70,7 @@ func TestCopilotWireProtocols(t *testing.T) { | |||
| 69 | 70 | if strings.Contains(string(body["system"]), "Claude Code") { | |
| 70 | 71 | t.Error("injected Claude Code system identity") | |
| 71 | 72 | } | |
| 72 | - | if tc.model == "claude-sonnet-5" { | |
| 73 | + | if tc.model == "claude-sonnet-5" || tc.model == "claude-opus-5.5" { | |
| 73 | 74 | if string(body["thinking"]) != `{"type":"adaptive"}` { | |
| 74 | 75 | t.Errorf("thinking = %s", body["thinking"]) | |
| 75 | 76 | } | |
| @@ -28,6 +28,7 @@ func seedCopilotTestToken(t *testing.T, baseURL string) string { | |||||
| 28 | func TestCopilotWireProtocols(t *testing.T) { | 28 | func TestCopilotWireProtocols(t *testing.T) { | ||
| 29 | for _, tc := range []struct{ model, path string }{ | 29 | for _, tc := range []struct{ model, path string }{ | ||
| 30 | {"claude-sonnet-5", "/v1/messages"}, | 30 | {"claude-sonnet-5", "/v1/messages"}, | ||
| 31 | + | {"claude-opus-5.5", "/v1/messages"}, | |||
| 31 | {"claude-haiku-4.5", "/v1/messages"}, | 32 | {"claude-haiku-4.5", "/v1/messages"}, | ||
| 32 | {"gpt-5.4", "/responses"}, | 33 | {"gpt-5.4", "/responses"}, | ||
| 33 | {"grok-4.6", "/responses"}, | 34 | {"grok-4.6", "/responses"}, | ||
| @@ -69,7 +70,7 @@ func TestCopilotWireProtocols(t *testing.T) { | |||||
| 69 | if strings.Contains(string(body["system"]), "Claude Code") { | 70 | if strings.Contains(string(body["system"]), "Claude Code") { | ||
| 70 | t.Error("injected Claude Code system identity") | 71 | t.Error("injected Claude Code system identity") | ||
| 71 | } | 72 | } | ||
| 72 | - | if tc.model == "claude-sonnet-5" { | 73 | + | if tc.model == "claude-sonnet-5" || tc.model == "claude-opus-5.5" { |
| 73 | if string(body["thinking"]) != `{"type":"adaptive"}` { | 74 | if string(body["thinking"]) != `{"type":"adaptive"}` { | ||
| 74 | t.Errorf("thinking = %s", body["thinking"]) | 75 | t.Errorf("thinking = %s", body["thinking"]) | ||
| 75 | } | 76 | } | ||
M packages/provider/models.go +5 -0
| @@ -302,6 +302,11 @@ var Catalog = []Model{ | |||
| 302 | 302 | ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, | |
| 303 | 303 | PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, | |
| 304 | 304 | }, | |
| 305 | + | { | |
| 306 | + | Provider: "anthropic", ID: "claude-opus-5-5", DisplayName: "Claude Opus 5.5", | |
| 307 | + | ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, | |
| 308 | + | PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, | |
| 309 | + | }, | |
| 305 | 310 | { | |
| 306 | 311 | Provider: "anthropic", ID: "claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6", | |
| 307 | 312 | ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, | |
| @@ -302,6 +302,11 @@ var Catalog = []Model{ | |||||
| 302 | ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, | 302 | ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, | ||
| 303 | PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, | 303 | PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, | ||
| 304 | }, | 304 | }, | ||
| 305 | + | { | |||
| 306 | + | Provider: "anthropic", ID: "claude-opus-5-5", DisplayName: "Claude Opus 5.5", | |||
| 307 | + | ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, | |||
| 308 | + | PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, | |||
| 309 | + | }, | |||
| 305 | { | 310 | { | ||
| 306 | Provider: "anthropic", ID: "claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6", | 311 | Provider: "anthropic", ID: "claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6", | ||
| 307 | ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, | 312 | ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, | ||
A packages/provider/opus55_test.go +73 -0
| @@ -0,0 +1,73 @@ | |||
| 1 | + | package provider | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "slices" | |
| 5 | + | "testing" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | func TestOpus55CatalogAndThinking(t *testing.T) { | |
| 9 | + | copilot := NewGithubCopilotClient("synthetic-token").(*copilotClient) | |
| 10 | + | for _, tc := range []struct { | |
| 11 | + | provider, id string | |
| 12 | + | client *anthropicClient | |
| 13 | + | }{ | |
| 14 | + | {"anthropic", "claude-opus-5-5", NewAnthropic("synthetic-key", "").(*anthropicClient)}, | |
| 15 | + | {"anthropic", "claude-opus-5-5", NewAnthropicOAuth("synthetic-token", "").(*anthropicClient)}, | |
| 16 | + | {"github-copilot", "claude-opus-5.5", copilot.router.byAPI[APIAnthropicMessages].(*anthropicClient)}, | |
| 17 | + | } { | |
| 18 | + | t.Run(tc.provider, func(t *testing.T) { | |
| 19 | + | m, err := FindModel(tc.provider, tc.id) | |
| 20 | + | if err != nil { | |
| 21 | + | t.Fatal(err) | |
| 22 | + | } | |
| 23 | + | if m.ContextWindow != 1000000 || m.MaxOutput != 128000 || !m.Reasoning || !m.AdaptiveThinking || m.Speculative { | |
| 24 | + | t.Fatalf("unexpected capabilities: %+v", m) | |
| 25 | + | } | |
| 26 | + | if m.PriceInput != 4 || m.PriceOutput != 20 || m.PriceCacheRead != 0.2 || m.PriceCacheWrite != 5 { | |
| 27 | + | t.Fatalf("unexpected pricing: %+v", m) | |
| 28 | + | } | |
| 29 | + | want := []string{"low", "medium", "high", "xhigh", "max"} | |
| 30 | + | if got := AvailableReasoningLevels(m); !slices.Equal(got, want) { | |
| 31 | + | t.Fatalf("levels = %q, want %q", got, want) | |
| 32 | + | } | |
| 33 | + | for _, level := range []string{"", "off", "minimum", "low", "medium", "high", "xhigh", "max"} { | |
| 34 | + | effort := level | |
| 35 | + | if level == "" || level == "off" || level == "minimum" { | |
| 36 | + | effort = "low" | |
| 37 | + | } | |
| 38 | + | if got := ClampReasoningForModel(m, level); got != effort { | |
| 39 | + | t.Errorf("clamp(%q) = %q, want %q", level, got, effort) | |
| 40 | + | } | |
| 41 | + | temperature := float32(0.5) | |
| 42 | + | wire, err := tc.client.buildRequest(Request{Model: tc.id, Reasoning: level, Temperature: &temperature}) | |
| 43 | + | if err != nil { | |
| 44 | + | t.Fatal(err) | |
| 45 | + | } | |
| 46 | + | if wire.Model != tc.id || wire.MaxTokens != 128000 || wire.Temperature != nil { | |
| 47 | + | t.Fatalf("unexpected request: %+v", wire) | |
| 48 | + | } | |
| 49 | + | if wire.Thinking == nil || wire.Thinking.Type != "adaptive" || wire.Thinking.BudgetTokens != 0 { | |
| 50 | + | t.Fatalf("thinking = %+v", wire.Thinking) | |
| 51 | + | } | |
| 52 | + | if wire.OutputConfig == nil || wire.OutputConfig.Effort != effort { | |
| 53 | + | t.Fatalf("output config = %+v, want effort %q", wire.OutputConfig, effort) | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | }) | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | func TestOpus55ReasoningOverrides(t *testing.T) { | |
| 61 | + | m, err := FindModel("anthropic", "claude-opus-5-5") | |
| 62 | + | if err != nil { | |
| 63 | + | t.Fatal(err) | |
| 64 | + | } | |
| 65 | + | m.ReasoningLevelMap = map[string]string{"max": "xhigh"} | |
| 66 | + | want := []string{"low", "medium", "high", "xhigh"} | |
| 67 | + | if got := AvailableReasoningLevels(m); !slices.Equal(got, want) { | |
| 68 | + | t.Fatalf("levels = %q, want %q", got, want) | |
| 69 | + | } | |
| 70 | + | if got := ClampReasoningForModel(m, "off"); got != "low" { | |
| 71 | + | t.Fatalf("off clamped to %q, want low", got) | |
| 72 | + | } | |
| 73 | + | } | |
| @@ -0,0 +1,73 @@ | |||||
| 1 | + | package provider | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | "testing" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | func TestOpus55CatalogAndThinking(t *testing.T) { | |||
| 9 | + | copilot := NewGithubCopilotClient("synthetic-token").(*copilotClient) | |||
| 10 | + | for _, tc := range []struct { | |||
| 11 | + | provider, id string | |||
| 12 | + | client *anthropicClient | |||
| 13 | + | }{ | |||
| 14 | + | {"anthropic", "claude-opus-5-5", NewAnthropic("synthetic-key", "").(*anthropicClient)}, | |||
| 15 | + | {"anthropic", "claude-opus-5-5", NewAnthropicOAuth("synthetic-token", "").(*anthropicClient)}, | |||
| 16 | + | {"github-copilot", "claude-opus-5.5", copilot.router.byAPI[APIAnthropicMessages].(*anthropicClient)}, | |||
| 17 | + | } { | |||
| 18 | + | t.Run(tc.provider, func(t *testing.T) { | |||
| 19 | + | m, err := FindModel(tc.provider, tc.id) | |||
| 20 | + | if err != nil { | |||
| 21 | + | t.Fatal(err) | |||
| 22 | + | } | |||
| 23 | + | if m.ContextWindow != 1000000 || m.MaxOutput != 128000 || !m.Reasoning || !m.AdaptiveThinking || m.Speculative { | |||
| 24 | + | t.Fatalf("unexpected capabilities: %+v", m) | |||
| 25 | + | } | |||
| 26 | + | if m.PriceInput != 4 || m.PriceOutput != 20 || m.PriceCacheRead != 0.2 || m.PriceCacheWrite != 5 { | |||
| 27 | + | t.Fatalf("unexpected pricing: %+v", m) | |||
| 28 | + | } | |||
| 29 | + | want := []string{"low", "medium", "high", "xhigh", "max"} | |||
| 30 | + | if got := AvailableReasoningLevels(m); !slices.Equal(got, want) { | |||
| 31 | + | t.Fatalf("levels = %q, want %q", got, want) | |||
| 32 | + | } | |||
| 33 | + | for _, level := range []string{"", "off", "minimum", "low", "medium", "high", "xhigh", "max"} { | |||
| 34 | + | effort := level | |||
| 35 | + | if level == "" || level == "off" || level == "minimum" { | |||
| 36 | + | effort = "low" | |||
| 37 | + | } | |||
| 38 | + | if got := ClampReasoningForModel(m, level); got != effort { | |||
| 39 | + | t.Errorf("clamp(%q) = %q, want %q", level, got, effort) | |||
| 40 | + | } | |||
| 41 | + | temperature := float32(0.5) | |||
| 42 | + | wire, err := tc.client.buildRequest(Request{Model: tc.id, Reasoning: level, Temperature: &temperature}) | |||
| 43 | + | if err != nil { | |||
| 44 | + | t.Fatal(err) | |||
| 45 | + | } | |||
| 46 | + | if wire.Model != tc.id || wire.MaxTokens != 128000 || wire.Temperature != nil { | |||
| 47 | + | t.Fatalf("unexpected request: %+v", wire) | |||
| 48 | + | } | |||
| 49 | + | if wire.Thinking == nil || wire.Thinking.Type != "adaptive" || wire.Thinking.BudgetTokens != 0 { | |||
| 50 | + | t.Fatalf("thinking = %+v", wire.Thinking) | |||
| 51 | + | } | |||
| 52 | + | if wire.OutputConfig == nil || wire.OutputConfig.Effort != effort { | |||
| 53 | + | t.Fatalf("output config = %+v, want effort %q", wire.OutputConfig, effort) | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | }) | |||
| 57 | + | } | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | func TestOpus55ReasoningOverrides(t *testing.T) { | |||
| 61 | + | m, err := FindModel("anthropic", "claude-opus-5-5") | |||
| 62 | + | if err != nil { | |||
| 63 | + | t.Fatal(err) | |||
| 64 | + | } | |||
| 65 | + | m.ReasoningLevelMap = map[string]string{"max": "xhigh"} | |||
| 66 | + | want := []string{"low", "medium", "high", "xhigh"} | |||
| 67 | + | if got := AvailableReasoningLevels(m); !slices.Equal(got, want) { | |||
| 68 | + | t.Fatalf("levels = %q, want %q", got, want) | |||
| 69 | + | } | |||
| 70 | + | if got := ClampReasoningForModel(m, "off"); got != "low" { | |||
| 71 | + | t.Fatalf("off clamped to %q, want low", got) | |||
| 72 | + | } | |||
| 73 | + | } | |||
M packages/provider/provider_test.go +2 -2
| @@ -105,11 +105,11 @@ func TestAnthropicOAuthUsesCurrentClaudeCodeVersion(t *testing.T) { | |||
| 105 | 105 | defer srv.Close() | |
| 106 | 106 | ||
| 107 | 107 | c := NewAnthropicOAuth("test-token", srv.URL) | |
| 108 | - | _, err := c.Stream(context.Background(), Request{Model: "claude-fable-5-1"}) | |
| 108 | + | _, err := c.Stream(context.Background(), Request{Model: "claude-opus-5-5"}) | |
| 109 | 109 | if err == nil { | |
| 110 | 110 | t.Fatal("expected test server error") | |
| 111 | 111 | } | |
| 112 | - | if want := "claude-cli/2.1.258"; userAgent != want { | |
| 112 | + | if want := "claude-cli/2.1.280"; userAgent != want { | |
| 113 | 113 | t.Fatalf("user-agent = %q, want %q", userAgent, want) | |
| 114 | 114 | } | |
| 115 | 115 | } | |
| @@ -105,11 +105,11 @@ func TestAnthropicOAuthUsesCurrentClaudeCodeVersion(t *testing.T) { | |||||
| 105 | defer srv.Close() | 105 | defer srv.Close() | ||
| 106 | 106 | ||||
| 107 | c := NewAnthropicOAuth("test-token", srv.URL) | 107 | c := NewAnthropicOAuth("test-token", srv.URL) | ||
| 108 | - | _, err := c.Stream(context.Background(), Request{Model: "claude-fable-5-1"}) | 108 | + | _, err := c.Stream(context.Background(), Request{Model: "claude-opus-5-5"}) |
| 109 | if err == nil { | 109 | if err == nil { | ||
| 110 | t.Fatal("expected test server error") | 110 | t.Fatal("expected test server error") | ||
| 111 | } | 111 | } | ||
| 112 | - | if want := "claude-cli/2.1.258"; userAgent != want { | 112 | + | if want := "claude-cli/2.1.280"; userAgent != want { |
| 113 | t.Fatalf("user-agent = %q, want %q", userAgent, want) | 113 | t.Fatalf("user-agent = %q, want %q", userAgent, want) | ||
| 114 | } | 114 | } | ||
| 115 | } | 115 | } | ||
M packages/provider/reasoning.go +17 -7
| @@ -13,7 +13,7 @@ func AvailableReasoningLevels(model Model) []string { | |||
| 13 | 13 | return defaults | |
| 14 | 14 | } | |
| 15 | 15 | ||
| 16 | - | available := map[string]bool{"": true} | |
| 16 | + | available := map[string]bool{"": containsReasoningLevel(defaults, "")} | |
| 17 | 17 | for _, level := range reasoningLevelOrder[1:] { | |
| 18 | 18 | if !containsReasoningLevel(defaults, level) { | |
| 19 | 19 | if _, overridden := model.ReasoningLevelMap[level]; !overridden { | |
| @@ -29,8 +29,8 @@ func AvailableReasoningLevels(model Model) []string { | |||
| 29 | 29 | } | |
| 30 | 30 | } | |
| 31 | 31 | ||
| 32 | - | levels := []string{""} | |
| 33 | - | for _, level := range reasoningLevelOrder[1:] { | |
| 32 | + | levels := []string{} | |
| 33 | + | for _, level := range reasoningLevelOrder { | |
| 34 | 34 | if available[level] { | |
| 35 | 35 | levels = append(levels, level) | |
| 36 | 36 | } | |
| @@ -44,6 +44,10 @@ func defaultReasoningLevels(model Model) []string { | |||
| 44 | 44 | } | |
| 45 | 45 | ||
| 46 | 46 | id := strings.ToLower(model.ID) | |
| 47 | + | // Opus 5.5 always uses adaptive thinking, including at its lowest effort. | |
| 48 | + | if id == "claude-opus-5-5" || id == "claude-opus-5.5" { | |
| 49 | + | return []string{"low", "medium", "high", "xhigh", "max"} | |
| 50 | + | } | |
| 47 | 51 | if (model.Provider == "google" || model.Provider == "google-vertex") && strings.Contains(id, "gemini-3") { | |
| 48 | 52 | if strings.Contains(id, "-pro") { | |
| 49 | 53 | return []string{"", "low", "high"} | |
| @@ -112,15 +116,21 @@ func ClampReasoningForModel(model Model, level string) string { | |||
| 112 | 116 | } | |
| 113 | 117 | ||
| 114 | 118 | func nearestReasoningLevel(available []string, requested string) string { | |
| 115 | - | if requested == "" || len(available) == 1 { | |
| 119 | + | if len(available) == 0 { | |
| 116 | 120 | return "" | |
| 117 | 121 | } | |
| 122 | + | if requested == "" || len(available) == 1 { | |
| 123 | + | return available[0] | |
| 124 | + | } | |
| 118 | 125 | requestedRank := reasoningLevelRank(requested) | |
| 119 | 126 | if requestedRank == 0 { | |
| 120 | - | return "" | |
| 127 | + | return available[0] | |
| 121 | 128 | } | |
| 122 | - | best, bestDistance := available[1], len(reasoningLevelOrder) | |
| 123 | - | for _, candidate := range available[1:] { | |
| 129 | + | best, bestDistance := available[0], len(reasoningLevelOrder) | |
| 130 | + | for _, candidate := range available { | |
| 131 | + | if candidate == "" { | |
| 132 | + | continue | |
| 133 | + | } | |
| 124 | 134 | distance := reasoningLevelRank(candidate) - requestedRank | |
| 125 | 135 | if distance < 0 { | |
| 126 | 136 | distance = -distance | |
| @@ -13,7 +13,7 @@ func AvailableReasoningLevels(model Model) []string { | |||||
| 13 | return defaults | 13 | return defaults | ||
| 14 | } | 14 | } | ||
| 15 | 15 | ||||
| 16 | - | available := map[string]bool{"": true} | 16 | + | available := map[string]bool{"": containsReasoningLevel(defaults, "")} |
| 17 | for _, level := range reasoningLevelOrder[1:] { | 17 | for _, level := range reasoningLevelOrder[1:] { | ||
| 18 | if !containsReasoningLevel(defaults, level) { | 18 | if !containsReasoningLevel(defaults, level) { | ||
| 19 | if _, overridden := model.ReasoningLevelMap[level]; !overridden { | 19 | if _, overridden := model.ReasoningLevelMap[level]; !overridden { | ||
| @@ -29,8 +29,8 @@ func AvailableReasoningLevels(model Model) []string { | |||||
| 29 | } | 29 | } | ||
| 30 | } | 30 | } | ||
| 31 | 31 | ||||
| 32 | - | levels := []string{""} | 32 | + | levels := []string{} |
| 33 | - | for _, level := range reasoningLevelOrder[1:] { | 33 | + | for _, level := range reasoningLevelOrder { |
| 34 | if available[level] { | 34 | if available[level] { | ||
| 35 | levels = append(levels, level) | 35 | levels = append(levels, level) | ||
| 36 | } | 36 | } | ||
| @@ -44,6 +44,10 @@ func defaultReasoningLevels(model Model) []string { | |||||
| 44 | } | 44 | } | ||
| 45 | 45 | ||||
| 46 | id := strings.ToLower(model.ID) | 46 | id := strings.ToLower(model.ID) | ||
| 47 | + | // Opus 5.5 always uses adaptive thinking, including at its lowest effort. | |||
| 48 | + | if id == "claude-opus-5-5" || id == "claude-opus-5.5" { | |||
| 49 | + | return []string{"low", "medium", "high", "xhigh", "max"} | |||
| 50 | + | } | |||
| 47 | if (model.Provider == "google" || model.Provider == "google-vertex") && strings.Contains(id, "gemini-3") { | 51 | if (model.Provider == "google" || model.Provider == "google-vertex") && strings.Contains(id, "gemini-3") { | ||
| 48 | if strings.Contains(id, "-pro") { | 52 | if strings.Contains(id, "-pro") { | ||
| 49 | return []string{"", "low", "high"} | 53 | return []string{"", "low", "high"} | ||
| @@ -112,15 +116,21 @@ func ClampReasoningForModel(model Model, level string) string { | |||||
| 112 | } | 116 | } | ||
| 113 | 117 | ||||
| 114 | func nearestReasoningLevel(available []string, requested string) string { | 118 | func nearestReasoningLevel(available []string, requested string) string { | ||
| 115 | - | if requested == "" || len(available) == 1 { | 119 | + | if len(available) == 0 { |
| 116 | return "" | 120 | return "" | ||
| 117 | } | 121 | } | ||
| 122 | + | if requested == "" || len(available) == 1 { | |||
| 123 | + | return available[0] | |||
| 124 | + | } | |||
| 118 | requestedRank := reasoningLevelRank(requested) | 125 | requestedRank := reasoningLevelRank(requested) | ||
| 119 | if requestedRank == 0 { | 126 | if requestedRank == 0 { | ||
| 120 | - | return "" | 127 | + | return available[0] |
| 121 | } | 128 | } | ||
| 122 | - | best, bestDistance := available[1], len(reasoningLevelOrder) | 129 | + | best, bestDistance := available[0], len(reasoningLevelOrder) |
| 123 | - | for _, candidate := range available[1:] { | 130 | + | for _, candidate := range available { |
| 131 | + | if candidate == "" { | |||
| 132 | + | continue | |||
| 133 | + | } | |||
| 124 | distance := reasoningLevelRank(candidate) - requestedRank | 134 | distance := reasoningLevelRank(candidate) - requestedRank | ||
| 125 | if distance < 0 { | 135 | if distance < 0 { | ||
| 126 | distance = -distance | 136 | distance = -distance | ||