detached e967eb8

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
748748
749749 > **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).
750750
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.
752754
753755 You can add additional Gemini model IDs to `models.json` under the `google` provider.
754756
@@ -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"
2121 // accepts the request; diverging from them causes 429 rate_limit_error
2222 // or 403 on the very first request.
2323 const (
24- claudeCodeVersion = "2.1.258"
24+ claudeCodeVersion = "2.1.280"
2525 claudeCodeIdentity = "You are Claude Code, Anthropic's official CLI for Claude."
2626 )
2727
@@ -21,7 +21,7 @@ const anthropicAPIVersion = "2023-06-01"
21 // accepts the request; diverging from them causes 429 rate_limit_error21 // 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{
153153 {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"},
154154 {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"},
155155 {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"},
156157 {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"},
157158 {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"},
158159 {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 (
77
88 func TestCopilotCatalogCoversPublicChatModels(t *testing.T) {
99 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"},
1111 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"},
1212 APICompletions: {"gemini-3.5-flash", "gemini-3.6-flash", "gemini-3.7-flash", "gemini-3.8-flash", "kimi-k2.7-code", "kimi-k3"},
1313 }
@@ -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) {
9797 t.Error("empty availability did not hide models")
9898 }
9999 SetModelAvailability("github-copilot", nil)
100- if len(ModelsForProvider("github-copilot")) != 27 {
100+ if len(ModelsForProvider("github-copilot")) != 28 {
101101 t.Error("clearing restriction did not restore catalog")
102102 }
103103 }
@@ -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 {
2828 func TestCopilotWireProtocols(t *testing.T) {
2929 for _, tc := range []struct{ model, path string }{
3030 {"claude-sonnet-5", "/v1/messages"},
31+ {"claude-opus-5.5", "/v1/messages"},
3132 {"claude-haiku-4.5", "/v1/messages"},
3233 {"gpt-5.4", "/responses"},
3334 {"grok-4.6", "/responses"},
@@ -69,7 +70,7 @@ func TestCopilotWireProtocols(t *testing.T) {
6970 if strings.Contains(string(body["system"]), "Claude Code") {
7071 t.Error("injected Claude Code system identity")
7172 }
72- if tc.model == "claude-sonnet-5" {
73+ if tc.model == "claude-sonnet-5" || tc.model == "claude-opus-5.5" {
7374 if string(body["thinking"]) != `{"type":"adaptive"}` {
7475 t.Errorf("thinking = %s", body["thinking"])
7576 }
@@ -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{
302302 ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
303303 PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
304304 },
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+ },
305310 {
306311 Provider: "anthropic", ID: "claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6",
307312 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) {
105105 defer srv.Close()
106106
107107 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"})
109109 if err == nil {
110110 t.Fatal("expected test server error")
111111 }
112- if want := "claude-cli/2.1.258"; userAgent != want {
112+ if want := "claude-cli/2.1.280"; userAgent != want {
113113 t.Fatalf("user-agent = %q, want %q", userAgent, want)
114114 }
115115 }
@@ -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 {
1313 return defaults
1414 }
1515
16- available := map[string]bool{"": true}
16+ available := map[string]bool{"": containsReasoningLevel(defaults, "")}
1717 for _, level := range reasoningLevelOrder[1:] {
1818 if !containsReasoningLevel(defaults, level) {
1919 if _, overridden := model.ReasoningLevelMap[level]; !overridden {
@@ -29,8 +29,8 @@ func AvailableReasoningLevels(model Model) []string {
2929 }
3030 }
3131
32- levels := []string{""}
33- for _, level := range reasoningLevelOrder[1:] {
32+ levels := []string{}
33+ for _, level := range reasoningLevelOrder {
3434 if available[level] {
3535 levels = append(levels, level)
3636 }
@@ -44,6 +44,10 @@ func defaultReasoningLevels(model Model) []string {
4444 }
4545
4646 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+ }
4751 if (model.Provider == "google" || model.Provider == "google-vertex") && strings.Contains(id, "gemini-3") {
4852 if strings.Contains(id, "-pro") {
4953 return []string{"", "low", "high"}
@@ -112,15 +116,21 @@ func ClampReasoningForModel(model Model, level string) string {
112116 }
113117
114118 func nearestReasoningLevel(available []string, requested string) string {
115- if requested == "" || len(available) == 1 {
119+ if len(available) == 0 {
116120 return ""
117121 }
122+ if requested == "" || len(available) == 1 {
123+ return available[0]
124+ }
118125 requestedRank := reasoningLevelRank(requested)
119126 if requestedRank == 0 {
120- return ""
127+ return available[0]
121128 }
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+ }
124134 distance := reasoningLevelRank(candidate) - requestedRank
125135 if distance < 0 {
126136 distance = -distance
@@ -13,7 +13,7 @@ func AvailableReasoningLevels(model Model) []string {
13 return defaults13 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) - requestedRank134 distance := reasoningLevelRank(candidate) - requestedRank
125 if distance < 0 {135 if distance < 0 {
126 distance = -distance136 distance = -distance

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close