fix(provider): align Astra specs and caching with release (#176)
- commit
- af6d63500f01e0789411f6cc2679c0daf4125963
- author
- bnema · · 2026-09-05 10:55 UTC
- committer
- GitHub · · 2026-09-05 10:55 UTC
- parent
- d92a60f
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
6 files · +127 -11
M docs/providers.md +26 -0
| @@ -148,6 +148,25 @@ provider currently returns an error instead of silently changing that provider's | |||
| 148 | 148 | request. Subagent children inherit the setting from their parent unless their | |
| 149 | 149 | profile sets `fastMode` explicitly. | |
| 150 | 150 | ||
| 151 | + | ## GPT-6 Astra | |
| 152 | + | ||
| 153 | + | `gpt-6-astra` is available through `openai` (Responses API with an API key) | |
| 154 | + | and `openai-codex` (ChatGPT subscription). Both catalog entries deliberately | |
| 155 | + | use a **500,000-token working context budget**, rather than the larger advertised | |
| 156 | + | maximum, and retain a 128,000-token maximum output. Native reasoning efforts | |
| 157 | + | `low`, `medium`, `high`, `xhigh`, and `max` are supported. Selecting reasoning off | |
| 158 | + | omits the effort parameter; it does not disable Astra's reasoning. | |
| 159 | + | ||
| 160 | + | Standard API rates per million tokens are $10 input, $50 output, $1 cache reads, | |
| 161 | + | and $12.50 cache writes. Above **272,000 total input tokens**, including cache | |
| 162 | + | reads and writes, rates for the entire request become $20 input, $75 output, | |
| 163 | + | $2 cache reads, and $25 cache writes. The 500k working budget does not avoid | |
| 164 | + | this pricing tier. Codex catalog dollar estimates use these API-equivalent | |
| 165 | + | rates, not subscription billing or quota measurements. | |
| 166 | + | ||
| 167 | + | Sources: [Astra model specifications](https://developers.openai.com/api/docs/models/gpt-6-astra) | |
| 168 | + | and [prompt caching](https://developers.openai.com/api/docs/guides/prompt-caching). | |
| 169 | + | ||
| 151 | 170 | ## Session identity and caching | |
| 152 | 171 | ||
| 153 | 172 | zut supplies every provider request with a root cache identity, a | |
| @@ -171,6 +190,13 @@ report cache detail. Cache writes remain separate from that hit rate. Provider | |||
| 171 | 190 | retention and eviction, request routing, and OpenAI's 1,024-token eligibility | |
| 172 | 191 | minimum can still produce a miss even when the stable prefix is unchanged. | |
| 173 | 192 | ||
| 193 | + | On the official public OpenAI Responses endpoint, GPT-5.6 and GPT-6 Astra | |
| 194 | + | requests include an explicit cache breakpoint after the stable instruction/tool | |
| 195 | + | prefix while retaining implicit caching for conversation history. The Codex | |
| 196 | + | subscription route and custom Responses endpoints do not receive these explicit | |
| 197 | + | cache controls. Changing request-level reasoning effort can still disrupt prefix | |
| 198 | + | reuse: zut does not yet send cache-preserving `configuration_update` items. | |
| 199 | + | ||
| 174 | 200 | ### OpenAI Responses WebSocket mode | |
| 175 | 201 | ||
| 176 | 202 | For the exact public endpoint `https://api.openai.com/v1/responses`, zut uses | |
| @@ -148,6 +148,25 @@ provider currently returns an error instead of silently changing that provider's | |||||
| 148 | request. Subagent children inherit the setting from their parent unless their | 148 | request. Subagent children inherit the setting from their parent unless their | ||
| 149 | profile sets `fastMode` explicitly. | 149 | profile sets `fastMode` explicitly. | ||
| 150 | 150 | ||||
| 151 | + | ## GPT-6 Astra | |||
| 152 | + | ||||
| 153 | + | `gpt-6-astra` is available through `openai` (Responses API with an API key) | |||
| 154 | + | and `openai-codex` (ChatGPT subscription). Both catalog entries deliberately | |||
| 155 | + | use a **500,000-token working context budget**, rather than the larger advertised | |||
| 156 | + | maximum, and retain a 128,000-token maximum output. Native reasoning efforts | |||
| 157 | + | `low`, `medium`, `high`, `xhigh`, and `max` are supported. Selecting reasoning off | |||
| 158 | + | omits the effort parameter; it does not disable Astra's reasoning. | |||
| 159 | + | ||||
| 160 | + | Standard API rates per million tokens are $10 input, $50 output, $1 cache reads, | |||
| 161 | + | and $12.50 cache writes. Above **272,000 total input tokens**, including cache | |||
| 162 | + | reads and writes, rates for the entire request become $20 input, $75 output, | |||
| 163 | + | $2 cache reads, and $25 cache writes. The 500k working budget does not avoid | |||
| 164 | + | this pricing tier. Codex catalog dollar estimates use these API-equivalent | |||
| 165 | + | rates, not subscription billing or quota measurements. | |||
| 166 | + | ||||
| 167 | + | Sources: [Astra model specifications](https://developers.openai.com/api/docs/models/gpt-6-astra) | |||
| 168 | + | and [prompt caching](https://developers.openai.com/api/docs/guides/prompt-caching). | |||
| 169 | + | ||||
| 151 | ## Session identity and caching | 170 | ## Session identity and caching | ||
| 152 | 171 | ||||
| 153 | zut supplies every provider request with a root cache identity, a | 172 | zut supplies every provider request with a root cache identity, a | ||
| @@ -171,6 +190,13 @@ report cache detail. Cache writes remain separate from that hit rate. Provider | |||||
| 171 | retention and eviction, request routing, and OpenAI's 1,024-token eligibility | 190 | retention and eviction, request routing, and OpenAI's 1,024-token eligibility | ||
| 172 | minimum can still produce a miss even when the stable prefix is unchanged. | 191 | minimum can still produce a miss even when the stable prefix is unchanged. | ||
| 173 | 192 | ||||
| 193 | + | On the official public OpenAI Responses endpoint, GPT-5.6 and GPT-6 Astra | |||
| 194 | + | requests include an explicit cache breakpoint after the stable instruction/tool | |||
| 195 | + | prefix while retaining implicit caching for conversation history. The Codex | |||
| 196 | + | subscription route and custom Responses endpoints do not receive these explicit | |||
| 197 | + | cache controls. Changing request-level reasoning effort can still disrupt prefix | |||
| 198 | + | reuse: zut does not yet send cache-preserving `configuration_update` items. | |||
| 199 | + | ||||
| 174 | ### OpenAI Responses WebSocket mode | 200 | ### OpenAI Responses WebSocket mode | ||
| 175 | 201 | ||||
| 176 | For the exact public endpoint `https://api.openai.com/v1/responses`, zut uses | 202 | For the exact public endpoint `https://api.openai.com/v1/responses`, zut uses | ||
A packages/provider/astra_test.go +85 -0
| @@ -0,0 +1,85 @@ | |||
| 1 | + | package provider | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "math" | |
| 5 | + | "slices" | |
| 6 | + | "testing" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestAstraReleasedMetadata(t *testing.T) { | |
| 10 | + | for _, name := range []string{"openai", "openai-codex"} { | |
| 11 | + | t.Run(name, func(t *testing.T) { | |
| 12 | + | m, err := FindModel(name, "gpt-6-astra") | |
| 13 | + | if err != nil { | |
| 14 | + | t.Fatal(err) | |
| 15 | + | } | |
| 16 | + | if m.ContextWindow != 500000 || m.MaxOutput != 128000 || m.Speculative { | |
| 17 | + | t.Fatalf("released model must retain the intentional 500k budget: %+v", m) | |
| 18 | + | } | |
| 19 | + | if name == "openai" && m.API != APIResponses { | |
| 20 | + | t.Fatalf("API = %q, want Responses", m.API) | |
| 21 | + | } | |
| 22 | + | if !slices.Contains(AvailableReasoningLevels(m), "max") || ClampReasoningForModel(m, "max") != "max" { | |
| 23 | + | t.Fatalf("native max unavailable: %v", AvailableReasoningLevels(m)) | |
| 24 | + | } | |
| 25 | + | if m.PriceTierInputTokens != 272000 || m.PriceInputAbove != 20 || m.PriceOutputAbove != 75 || m.PriceCacheReadAbove != 2 || m.PriceCacheWriteAbove != 25 { | |
| 26 | + | t.Fatalf("long-context rates: %+v", m) | |
| 27 | + | } | |
| 28 | + | for _, tc := range []struct { | |
| 29 | + | name string | |
| 30 | + | usage Usage | |
| 31 | + | want float64 | |
| 32 | + | }{ | |
| 33 | + | {"at_threshold", Usage{InputTokens: 100000, CacheReadTokens: 100000, CacheWriteTokens: 72000, OutputTokens: 1000}, 2.05}, | |
| 34 | + | {"above_threshold", Usage{InputTokens: 100000, CacheReadTokens: 100001, CacheWriteTokens: 72000, OutputTokens: 1000}, 4.075002}, | |
| 35 | + | } { | |
| 36 | + | t.Run(tc.name, func(t *testing.T) { | |
| 37 | + | if got := ComputeCost(m, tc.usage); math.Abs(got-tc.want) > 1e-9 { | |
| 38 | + | t.Fatalf("cost = %.9f, want %.9f", got, tc.want) | |
| 39 | + | } | |
| 40 | + | }) | |
| 41 | + | } | |
| 42 | + | }) | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func TestAstraResponsesCapabilities(t *testing.T) { | |
| 47 | + | public := NewOpenAIResponsesNamed("synthetic-token", "", "openai").(*renamedClient).inner.(*responsesWebSocketClient).http | |
| 48 | + | custom := NewOpenAIResponsesNamed("synthetic-token", "https://example.com/v1/responses", "openai").(*renamedClient).inner.(*codexClient) | |
| 49 | + | for _, tc := range []struct { | |
| 50 | + | name string | |
| 51 | + | client *codexClient | |
| 52 | + | breakpoint bool | |
| 53 | + | }{ | |
| 54 | + | {"public", public, true}, | |
| 55 | + | {"subscription", newOpenAICodexClient("synthetic-token", "synthetic-account", ""), false}, | |
| 56 | + | {"custom", custom, false}, | |
| 57 | + | } { | |
| 58 | + | t.Run(tc.name, func(t *testing.T) { | |
| 59 | + | wire, err := tc.client.buildRequest(Request{Model: "gpt-6-astra", Reasoning: "max", Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hello"}}}}}) | |
| 60 | + | if err != nil { | |
| 61 | + | t.Fatal(err) | |
| 62 | + | } | |
| 63 | + | if wire.Reasoning == nil || wire.Reasoning.Effort != "max" { | |
| 64 | + | t.Fatalf("reasoning = %+v, want max", wire.Reasoning) | |
| 65 | + | } | |
| 66 | + | if !tc.breakpoint { | |
| 67 | + | if wire.PromptCacheOptions != nil || len(wire.Input) != 1 { | |
| 68 | + | t.Fatalf("undeclared endpoint received explicit caching: %+v", wire) | |
| 69 | + | } | |
| 70 | + | return | |
| 71 | + | } | |
| 72 | + | if wire.PromptCacheOptions == nil || wire.PromptCacheOptions.Mode != "implicit" || len(wire.Input) != 2 { | |
| 73 | + | t.Fatalf("missing stable boundary with implicit history caching: %+v", wire) | |
| 74 | + | } | |
| 75 | + | boundary, ok := wire.Input[0].(codexInputMessage) | |
| 76 | + | if !ok || boundary.Role != "developer" || len(boundary.Content) != 1 { | |
| 77 | + | t.Fatalf("boundary = %#v", wire.Input[0]) | |
| 78 | + | } | |
| 79 | + | text, ok := boundary.Content[0].(codexInputText) | |
| 80 | + | if !ok || text.PromptCacheBreakpoint == nil || text.PromptCacheBreakpoint.Mode != "explicit" { | |
| 81 | + | t.Fatalf("breakpoint = %#v", boundary.Content[0]) | |
| 82 | + | } | |
| 83 | + | }) | |
| 84 | + | } | |
| 85 | + | } | |
| @@ -0,0 +1,85 @@ | |||||
| 1 | + | package provider | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "math" | |||
| 5 | + | "slices" | |||
| 6 | + | "testing" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestAstraReleasedMetadata(t *testing.T) { | |||
| 10 | + | for _, name := range []string{"openai", "openai-codex"} { | |||
| 11 | + | t.Run(name, func(t *testing.T) { | |||
| 12 | + | m, err := FindModel(name, "gpt-6-astra") | |||
| 13 | + | if err != nil { | |||
| 14 | + | t.Fatal(err) | |||
| 15 | + | } | |||
| 16 | + | if m.ContextWindow != 500000 || m.MaxOutput != 128000 || m.Speculative { | |||
| 17 | + | t.Fatalf("released model must retain the intentional 500k budget: %+v", m) | |||
| 18 | + | } | |||
| 19 | + | if name == "openai" && m.API != APIResponses { | |||
| 20 | + | t.Fatalf("API = %q, want Responses", m.API) | |||
| 21 | + | } | |||
| 22 | + | if !slices.Contains(AvailableReasoningLevels(m), "max") || ClampReasoningForModel(m, "max") != "max" { | |||
| 23 | + | t.Fatalf("native max unavailable: %v", AvailableReasoningLevels(m)) | |||
| 24 | + | } | |||
| 25 | + | if m.PriceTierInputTokens != 272000 || m.PriceInputAbove != 20 || m.PriceOutputAbove != 75 || m.PriceCacheReadAbove != 2 || m.PriceCacheWriteAbove != 25 { | |||
| 26 | + | t.Fatalf("long-context rates: %+v", m) | |||
| 27 | + | } | |||
| 28 | + | for _, tc := range []struct { | |||
| 29 | + | name string | |||
| 30 | + | usage Usage | |||
| 31 | + | want float64 | |||
| 32 | + | }{ | |||
| 33 | + | {"at_threshold", Usage{InputTokens: 100000, CacheReadTokens: 100000, CacheWriteTokens: 72000, OutputTokens: 1000}, 2.05}, | |||
| 34 | + | {"above_threshold", Usage{InputTokens: 100000, CacheReadTokens: 100001, CacheWriteTokens: 72000, OutputTokens: 1000}, 4.075002}, | |||
| 35 | + | } { | |||
| 36 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 37 | + | if got := ComputeCost(m, tc.usage); math.Abs(got-tc.want) > 1e-9 { | |||
| 38 | + | t.Fatalf("cost = %.9f, want %.9f", got, tc.want) | |||
| 39 | + | } | |||
| 40 | + | }) | |||
| 41 | + | } | |||
| 42 | + | }) | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | func TestAstraResponsesCapabilities(t *testing.T) { | |||
| 47 | + | public := NewOpenAIResponsesNamed("synthetic-token", "", "openai").(*renamedClient).inner.(*responsesWebSocketClient).http | |||
| 48 | + | custom := NewOpenAIResponsesNamed("synthetic-token", "https://example.com/v1/responses", "openai").(*renamedClient).inner.(*codexClient) | |||
| 49 | + | for _, tc := range []struct { | |||
| 50 | + | name string | |||
| 51 | + | client *codexClient | |||
| 52 | + | breakpoint bool | |||
| 53 | + | }{ | |||
| 54 | + | {"public", public, true}, | |||
| 55 | + | {"subscription", newOpenAICodexClient("synthetic-token", "synthetic-account", ""), false}, | |||
| 56 | + | {"custom", custom, false}, | |||
| 57 | + | } { | |||
| 58 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 59 | + | wire, err := tc.client.buildRequest(Request{Model: "gpt-6-astra", Reasoning: "max", Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hello"}}}}}) | |||
| 60 | + | if err != nil { | |||
| 61 | + | t.Fatal(err) | |||
| 62 | + | } | |||
| 63 | + | if wire.Reasoning == nil || wire.Reasoning.Effort != "max" { | |||
| 64 | + | t.Fatalf("reasoning = %+v, want max", wire.Reasoning) | |||
| 65 | + | } | |||
| 66 | + | if !tc.breakpoint { | |||
| 67 | + | if wire.PromptCacheOptions != nil || len(wire.Input) != 1 { | |||
| 68 | + | t.Fatalf("undeclared endpoint received explicit caching: %+v", wire) | |||
| 69 | + | } | |||
| 70 | + | return | |||
| 71 | + | } | |||
| 72 | + | if wire.PromptCacheOptions == nil || wire.PromptCacheOptions.Mode != "implicit" || len(wire.Input) != 2 { | |||
| 73 | + | t.Fatalf("missing stable boundary with implicit history caching: %+v", wire) | |||
| 74 | + | } | |||
| 75 | + | boundary, ok := wire.Input[0].(codexInputMessage) | |||
| 76 | + | if !ok || boundary.Role != "developer" || len(boundary.Content) != 1 { | |||
| 77 | + | t.Fatalf("boundary = %#v", wire.Input[0]) | |||
| 78 | + | } | |||
| 79 | + | text, ok := boundary.Content[0].(codexInputText) | |||
| 80 | + | if !ok || text.PromptCacheBreakpoint == nil || text.PromptCacheBreakpoint.Mode != "explicit" { | |||
| 81 | + | t.Fatalf("breakpoint = %#v", boundary.Content[0]) | |||
| 82 | + | } | |||
| 83 | + | }) | |||
| 84 | + | } | |||
| 85 | + | } | |||
M packages/provider/models.go +7 -3
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "sync" | |
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | + | // Deliberate working budget, not the models' advertised maximum context. | |
| 9 | 10 | const openAIContextWindowTarget = 500_000 | |
| 10 | 11 | ||
| 11 | 12 | // Model describes a single LLM we know about. | |
| @@ -358,8 +359,9 @@ var Catalog = []Model{ | |||
| 358 | 359 | { | |
| 359 | 360 | Provider: "openai", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra", API: APIResponses, | |
| 360 | 361 | ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true, | |
| 361 | - | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, | |
| 362 | - | Speculative: true, | |
| 362 | + | PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5, | |
| 363 | + | PriceTierInputTokens: 272000, | |
| 364 | + | PriceInputAbove: 20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25, | |
| 363 | 365 | }, | |
| 364 | 366 | // ---- OpenAI Codex / ChatGPT subscription backend ---- | |
| 365 | 367 | // Same model ids as the OpenAI family, but routed through the | |
| @@ -406,7 +408,9 @@ var Catalog = []Model{ | |||
| 406 | 408 | { | |
| 407 | 409 | Provider: "openai-codex", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra", | |
| 408 | 410 | ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true, | |
| 409 | - | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, | |
| 411 | + | PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5, | |
| 412 | + | PriceTierInputTokens: 272000, | |
| 413 | + | PriceInputAbove: 20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25, | |
| 410 | 414 | }, | |
| 411 | 415 | } | |
| 412 | 416 | ||
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "sync" | 6 | "sync" | ||
| 7 | ) | 7 | ) | ||
| 8 | 8 | ||||
| 9 | + | // Deliberate working budget, not the models' advertised maximum context. | |||
| 9 | const openAIContextWindowTarget = 500_000 | 10 | const openAIContextWindowTarget = 500_000 | ||
| 10 | 11 | ||||
| 11 | // Model describes a single LLM we know about. | 12 | // Model describes a single LLM we know about. | ||
| @@ -358,8 +359,9 @@ var Catalog = []Model{ | |||||
| 358 | { | 359 | { | ||
| 359 | Provider: "openai", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra", API: APIResponses, | 360 | Provider: "openai", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra", API: APIResponses, | ||
| 360 | ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true, | 361 | ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true, | ||
| 361 | - | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, | 362 | + | PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5, |
| 362 | - | Speculative: true, | 363 | + | PriceTierInputTokens: 272000, |
| 364 | + | PriceInputAbove: 20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25, | |||
| 363 | }, | 365 | }, | ||
| 364 | // ---- OpenAI Codex / ChatGPT subscription backend ---- | 366 | // ---- OpenAI Codex / ChatGPT subscription backend ---- | ||
| 365 | // Same model ids as the OpenAI family, but routed through the | 367 | // Same model ids as the OpenAI family, but routed through the | ||
| @@ -406,7 +408,9 @@ var Catalog = []Model{ | |||||
| 406 | { | 408 | { | ||
| 407 | Provider: "openai-codex", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra", | 409 | Provider: "openai-codex", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra", | ||
| 408 | ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true, | 410 | ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true, | ||
| 409 | - | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, | 411 | + | PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5, |
| 412 | + | PriceTierInputTokens: 272000, | |||
| 413 | + | PriceInputAbove: 20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25, | |||
| 410 | }, | 414 | }, | ||
| 411 | } | 415 | } | ||
| 412 | 416 | ||||
M packages/provider/openai_codex.go +4 -3
| @@ -214,8 +214,9 @@ func (c *codexClient) findModel(id string) (Model, error) { | |||
| 214 | 214 | return FindModel("openai", id) | |
| 215 | 215 | } | |
| 216 | 216 | ||
| 217 | - | func isGPT56Model(model string) bool { | |
| 218 | - | return strings.HasPrefix(strings.ToLower(strings.TrimSpace(model)), "gpt-5.6-") | |
| 217 | + | func supportsOpenAIExplicitPromptCache(model string) bool { | |
| 218 | + | id := strings.ToLower(strings.TrimSpace(model)) | |
| 219 | + | return strings.HasPrefix(id, "gpt-5.6-") || id == "gpt-6-astra" | |
| 219 | 220 | } | |
| 220 | 221 | ||
| 221 | 222 | func (c *codexClient) buildRequest(req Request) (*codexRequest, error) { | |
| @@ -243,7 +244,7 @@ func (c *codexClient) buildRequest(req Request) (*codexRequest, error) { | |||
| 243 | 244 | if req.FastMode { | |
| 244 | 245 | body.ServiceTier = fastModeServiceTier | |
| 245 | 246 | } | |
| 246 | - | if c.capabilities.ExplicitPromptCacheBreakpoint && isGPT56Model(req.Model) { | |
| 247 | + | if c.capabilities.ExplicitPromptCacheBreakpoint && supportsOpenAIExplicitPromptCache(req.Model) { | |
| 247 | 248 | // Keep implicit mode so OpenAI can retain later conversation | |
| 248 | 249 | // checkpoints as well as this stable instruction/tool boundary. | |
| 249 | 250 | body.PromptCacheOptions = &codexPromptCacheOptions{Mode: "implicit"} | |
| @@ -214,8 +214,9 @@ func (c *codexClient) findModel(id string) (Model, error) { | |||||
| 214 | return FindModel("openai", id) | 214 | return FindModel("openai", id) | ||
| 215 | } | 215 | } | ||
| 216 | 216 | ||||
| 217 | - | func isGPT56Model(model string) bool { | 217 | + | func supportsOpenAIExplicitPromptCache(model string) bool { |
| 218 | - | return strings.HasPrefix(strings.ToLower(strings.TrimSpace(model)), "gpt-5.6-") | 218 | + | id := strings.ToLower(strings.TrimSpace(model)) |
| 219 | + | return strings.HasPrefix(id, "gpt-5.6-") || id == "gpt-6-astra" | |||
| 219 | } | 220 | } | ||
| 220 | 221 | ||||
| 221 | func (c *codexClient) buildRequest(req Request) (*codexRequest, error) { | 222 | func (c *codexClient) buildRequest(req Request) (*codexRequest, error) { | ||
| @@ -243,7 +244,7 @@ func (c *codexClient) buildRequest(req Request) (*codexRequest, error) { | |||||
| 243 | if req.FastMode { | 244 | if req.FastMode { | ||
| 244 | body.ServiceTier = fastModeServiceTier | 245 | body.ServiceTier = fastModeServiceTier | ||
| 245 | } | 246 | } | ||
| 246 | - | if c.capabilities.ExplicitPromptCacheBreakpoint && isGPT56Model(req.Model) { | 247 | + | if c.capabilities.ExplicitPromptCacheBreakpoint && supportsOpenAIExplicitPromptCache(req.Model) { |
| 247 | // Keep implicit mode so OpenAI can retain later conversation | 248 | // Keep implicit mode so OpenAI can retain later conversation | ||
| 248 | // checkpoints as well as this stable instruction/tool boundary. | 249 | // checkpoints as well as this stable instruction/tool boundary. | ||
| 249 | body.PromptCacheOptions = &codexPromptCacheOptions{Mode: "implicit"} | 250 | body.PromptCacheOptions = &codexPromptCacheOptions{Mode: "implicit"} | ||
M packages/provider/provider_test.go +2 -2
| @@ -658,11 +658,11 @@ func TestGPT56AndGPT6CatalogEntries(t *testing.T) { | |||
| 658 | 658 | {"openai", "gpt-5.6-luna", 500000, 1, 6, 0.1, 1.25}, | |
| 659 | 659 | {"openai", "gpt-5.6-sol", 500000, 5, 30, 0.5, 6.25}, | |
| 660 | 660 | {"openai", "gpt-5.6-terra", 500000, 2.5, 15, 0.25, 3.125}, | |
| 661 | - | {"openai", "gpt-6-astra", 500000, 5, 30, 0.5, 6.25}, | |
| 661 | + | {"openai", "gpt-6-astra", 500000, 10, 50, 1, 12.5}, | |
| 662 | 662 | {"openai-codex", "gpt-5.6-luna", 500000, 1, 6, 0.1, 1.25}, | |
| 663 | 663 | {"openai-codex", "gpt-5.6-sol", 500000, 5, 30, 0.5, 6.25}, | |
| 664 | 664 | {"openai-codex", "gpt-5.6-terra", 500000, 2.5, 15, 0.25, 3.125}, | |
| 665 | - | {"openai-codex", "gpt-6-astra", 500000, 5, 30, 0.5, 6.25}, | |
| 665 | + | {"openai-codex", "gpt-6-astra", 500000, 10, 50, 1, 12.5}, | |
| 666 | 666 | {"azure-openai-responses", "gpt-5.6-luna", 1050000, 1, 6, 0.1, 1.25}, | |
| 667 | 667 | {"azure-openai-responses", "gpt-5.6-sol", 1050000, 5, 30, 0.5, 6.25}, | |
| 668 | 668 | {"azure-openai-responses", "gpt-5.6-terra", 1050000, 2.5, 15, 0.25, 3.125}, | |
| @@ -658,11 +658,11 @@ func TestGPT56AndGPT6CatalogEntries(t *testing.T) { | |||||
| 658 | {"openai", "gpt-5.6-luna", 500000, 1, 6, 0.1, 1.25}, | 658 | {"openai", "gpt-5.6-luna", 500000, 1, 6, 0.1, 1.25}, | ||
| 659 | {"openai", "gpt-5.6-sol", 500000, 5, 30, 0.5, 6.25}, | 659 | {"openai", "gpt-5.6-sol", 500000, 5, 30, 0.5, 6.25}, | ||
| 660 | {"openai", "gpt-5.6-terra", 500000, 2.5, 15, 0.25, 3.125}, | 660 | {"openai", "gpt-5.6-terra", 500000, 2.5, 15, 0.25, 3.125}, | ||
| 661 | - | {"openai", "gpt-6-astra", 500000, 5, 30, 0.5, 6.25}, | 661 | + | {"openai", "gpt-6-astra", 500000, 10, 50, 1, 12.5}, |
| 662 | {"openai-codex", "gpt-5.6-luna", 500000, 1, 6, 0.1, 1.25}, | 662 | {"openai-codex", "gpt-5.6-luna", 500000, 1, 6, 0.1, 1.25}, | ||
| 663 | {"openai-codex", "gpt-5.6-sol", 500000, 5, 30, 0.5, 6.25}, | 663 | {"openai-codex", "gpt-5.6-sol", 500000, 5, 30, 0.5, 6.25}, | ||
| 664 | {"openai-codex", "gpt-5.6-terra", 500000, 2.5, 15, 0.25, 3.125}, | 664 | {"openai-codex", "gpt-5.6-terra", 500000, 2.5, 15, 0.25, 3.125}, | ||
| 665 | - | {"openai-codex", "gpt-6-astra", 500000, 5, 30, 0.5, 6.25}, | 665 | + | {"openai-codex", "gpt-6-astra", 500000, 10, 50, 1, 12.5}, |
| 666 | {"azure-openai-responses", "gpt-5.6-luna", 1050000, 1, 6, 0.1, 1.25}, | 666 | {"azure-openai-responses", "gpt-5.6-luna", 1050000, 1, 6, 0.1, 1.25}, | ||
| 667 | {"azure-openai-responses", "gpt-5.6-sol", 1050000, 5, 30, 0.5, 6.25}, | 667 | {"azure-openai-responses", "gpt-5.6-sol", 1050000, 5, 30, 0.5, 6.25}, | ||
| 668 | {"azure-openai-responses", "gpt-5.6-terra", 1050000, 2.5, 15, 0.25, 3.125}, | 668 | {"azure-openai-responses", "gpt-5.6-terra", 1050000, 2.5, 15, 0.25, 3.125}, | ||
M packages/provider/reasoning.go +3 -3
| @@ -58,7 +58,7 @@ func defaultReasoningLevels(model Model) []string { | |||
| 58 | 58 | } | |
| 59 | 59 | if model.API == APIResponses || model.Provider == "openai-codex" || model.Provider == "openai-responses" || model.Provider == "azure-openai-responses" { | |
| 60 | 60 | levels := []string{"", "low", "medium", "high", "xhigh"} | |
| 61 | - | if strings.HasPrefix(id, "gpt-5.6-") { | |
| 61 | + | if strings.HasPrefix(id, "gpt-5.6-") || id == "gpt-6-astra" { | |
| 62 | 62 | levels = append(levels, "max") | |
| 63 | 63 | } | |
| 64 | 64 | return levels | |
| @@ -249,7 +249,7 @@ func OpenAICompatAnthropicEffort(level string) string { | |||
| 249 | 249 | } | |
| 250 | 250 | ||
| 251 | 251 | // OpenAICodexReasoningEffort maps zut levels onto the Responses API effort | |
| 252 | - | // enum. GPT-5.6 supports native max; other models clamp max to xhigh. | |
| 252 | + | // enum. GPT-5.6 and GPT-6 Astra support native max; other models clamp to xhigh. | |
| 253 | 253 | func OpenAICodexReasoningEffort(level, model string) string { | |
| 254 | 254 | switch NormalizeReasoning(level) { | |
| 255 | 255 | case "minimum", "low": | |
| @@ -261,7 +261,7 @@ func OpenAICodexReasoningEffort(level, model string) string { | |||
| 261 | 261 | case "xhigh": | |
| 262 | 262 | return "xhigh" | |
| 263 | 263 | case "max": | |
| 264 | - | if strings.HasPrefix(strings.ToLower(model), "gpt-5.6-") { | |
| 264 | + | if strings.HasPrefix(strings.ToLower(model), "gpt-5.6-") || strings.EqualFold(model, "gpt-6-astra") { | |
| 265 | 265 | return "max" | |
| 266 | 266 | } | |
| 267 | 267 | return "xhigh" | |
| @@ -58,7 +58,7 @@ func defaultReasoningLevels(model Model) []string { | |||||
| 58 | } | 58 | } | ||
| 59 | if model.API == APIResponses || model.Provider == "openai-codex" || model.Provider == "openai-responses" || model.Provider == "azure-openai-responses" { | 59 | if model.API == APIResponses || model.Provider == "openai-codex" || model.Provider == "openai-responses" || model.Provider == "azure-openai-responses" { | ||
| 60 | levels := []string{"", "low", "medium", "high", "xhigh"} | 60 | levels := []string{"", "low", "medium", "high", "xhigh"} | ||
| 61 | - | if strings.HasPrefix(id, "gpt-5.6-") { | 61 | + | if strings.HasPrefix(id, "gpt-5.6-") || id == "gpt-6-astra" { |
| 62 | levels = append(levels, "max") | 62 | levels = append(levels, "max") | ||
| 63 | } | 63 | } | ||
| 64 | return levels | 64 | return levels | ||
| @@ -249,7 +249,7 @@ func OpenAICompatAnthropicEffort(level string) string { | |||||
| 249 | } | 249 | } | ||
| 250 | 250 | ||||
| 251 | // OpenAICodexReasoningEffort maps zut levels onto the Responses API effort | 251 | // OpenAICodexReasoningEffort maps zut levels onto the Responses API effort | ||
| 252 | - | // enum. GPT-5.6 supports native max; other models clamp max to xhigh. | 252 | + | // enum. GPT-5.6 and GPT-6 Astra support native max; other models clamp to xhigh. |
| 253 | func OpenAICodexReasoningEffort(level, model string) string { | 253 | func OpenAICodexReasoningEffort(level, model string) string { | ||
| 254 | switch NormalizeReasoning(level) { | 254 | switch NormalizeReasoning(level) { | ||
| 255 | case "minimum", "low": | 255 | case "minimum", "low": | ||
| @@ -261,7 +261,7 @@ func OpenAICodexReasoningEffort(level, model string) string { | |||||
| 261 | case "xhigh": | 261 | case "xhigh": | ||
| 262 | return "xhigh" | 262 | return "xhigh" | ||
| 263 | case "max": | 263 | case "max": | ||
| 264 | - | if strings.HasPrefix(strings.ToLower(model), "gpt-5.6-") { | 264 | + | if strings.HasPrefix(strings.ToLower(model), "gpt-5.6-") || strings.EqualFold(model, "gpt-6-astra") { |
| 265 | return "max" | 265 | return "max" | ||
| 266 | } | 266 | } | ||
| 267 | return "xhigh" | 267 | return "xhigh" | ||