Merge pull request #167 from dariopb/main
Add GPT-6 Astra support across OpenAI and Copilot providers
9 files · +141 -8
M docs/providers.md +19 -0
| @@ -90,6 +90,25 @@ These providers support subscription login: | |||
| 90 | 90 | OAuth tokens are stored in `$ZOT_HOME/auth.json` and refreshed when refresh is | |
| 91 | 91 | available. | |
| 92 | 92 | ||
| 93 | + | ## GPT-6 Astra | |
| 94 | + | ||
| 95 | + | Select `gpt-6-astra` with provider `openai`, `openai-responses`, | |
| 96 | + | `openai-codex`, or `github-copilot`, for example: | |
| 97 | + | ||
| 98 | + | ```bash | |
| 99 | + | zot --provider github-copilot --model gpt-6-astra | |
| 100 | + | ``` | |
| 101 | + | ||
| 102 | + | Astra uses the Responses API and supports `low`, `medium`, `high`, `xhigh`, | |
| 103 | + | and `max` reasoning. Public API and Copilot catalog entries include its | |
| 104 | + | 1,050,000-token context window; the OpenAI Codex subscription entry uses the | |
| 105 | + | backend's 272,000-token default. All entries include the 128,000-token output | |
| 106 | + | limit and standard token prices, including the higher rates for prompts above | |
| 107 | + | 272,000 tokens. Subscription access depends on your | |
| 108 | + | plan, rollout, and organization policy; catalog availability does not grant access. | |
| 109 | + | See the [OpenAI model specifications](https://developers.openai.com/api/docs/models/gpt-6-astra) | |
| 110 | + | and [GitHub Copilot availability](https://github.blog/changelog/2026-09-04-gpt-6-astra-is-generally-available-in-github-copilot/). | |
| 111 | + | ||
| 93 | 112 | ## API-key providers | |
| 94 | 113 | ||
| 95 | 114 | These providers can use environment variables. Simple API-key providers can | |
| @@ -90,6 +90,25 @@ These providers support subscription login: | |||||
| 90 | OAuth tokens are stored in `$ZOT_HOME/auth.json` and refreshed when refresh is | 90 | OAuth tokens are stored in `$ZOT_HOME/auth.json` and refreshed when refresh is | ||
| 91 | available. | 91 | available. | ||
| 92 | 92 | ||||
| 93 | + | ## GPT-6 Astra | |||
| 94 | + | ||||
| 95 | + | Select `gpt-6-astra` with provider `openai`, `openai-responses`, | |||
| 96 | + | `openai-codex`, or `github-copilot`, for example: | |||
| 97 | + | ||||
| 98 | + | ```bash | |||
| 99 | + | zot --provider github-copilot --model gpt-6-astra | |||
| 100 | + | ``` | |||
| 101 | + | ||||
| 102 | + | Astra uses the Responses API and supports `low`, `medium`, `high`, `xhigh`, | |||
| 103 | + | and `max` reasoning. Public API and Copilot catalog entries include its | |||
| 104 | + | 1,050,000-token context window; the OpenAI Codex subscription entry uses the | |||
| 105 | + | backend's 272,000-token default. All entries include the 128,000-token output | |||
| 106 | + | limit and standard token prices, including the higher rates for prompts above | |||
| 107 | + | 272,000 tokens. Subscription access depends on your | |||
| 108 | + | plan, rollout, and organization policy; catalog availability does not grant access. | |||
| 109 | + | See the [OpenAI model specifications](https://developers.openai.com/api/docs/models/gpt-6-astra) | |||
| 110 | + | and [GitHub Copilot availability](https://github.blog/changelog/2026-09-04-gpt-6-astra-is-generally-available-in-github-copilot/). | |||
| 111 | + | ||||
| 93 | ## API-key providers | 112 | ## API-key providers | ||
| 94 | 113 | ||||
| 95 | These providers can use environment variables. Simple API-key providers can | 114 | These providers can use environment variables. Simple API-key providers can | ||
M packages/provider/catalog_builtin.go +1 -0
| @@ -163,6 +163,7 @@ var builtinCatalog = []Model{ | |||
| 163 | 163 | {Provider: "github-copilot", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol", API: APIResponses, ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | |
| 164 | 164 | {Provider: "github-copilot", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra", API: APIResponses, ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | |
| 165 | 165 | {Provider: "github-copilot", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna", API: APIResponses, ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | |
| 166 | + | {Provider: "github-copilot", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra", API: APIResponses, ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5, PriceTierInputTokens: 272000, PriceInputAbove: 20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25, BaseURL: "https://api.individual.githubcopilot.com"}, | |
| 166 | 167 | {Provider: "github-copilot", ID: "grok-code-fast-1", DisplayName: "Grok Code Fast 1", ContextWindow: 128000, MaxOutput: 64000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | |
| 167 | 168 | // ----- google ----- | |
| 168 | 169 | {Provider: "google", ID: "gemini-3-flash-preview", DisplayName: "Gemini 3 Flash Preview", ContextWindow: 1048576, MaxOutput: 65536, Reasoning: true, PriceInput: 0.5, PriceOutput: 3, PriceCacheRead: 0.05, BaseURL: "https://generativelanguage.googleapis.com/v1beta"}, | |
| @@ -163,6 +163,7 @@ var builtinCatalog = []Model{ | |||||
| 163 | {Provider: "github-copilot", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol", API: APIResponses, ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | 163 | {Provider: "github-copilot", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol", API: APIResponses, ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | ||
| 164 | {Provider: "github-copilot", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra", API: APIResponses, ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | 164 | {Provider: "github-copilot", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra", API: APIResponses, ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | ||
| 165 | {Provider: "github-copilot", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna", API: APIResponses, ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | 165 | {Provider: "github-copilot", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna", API: APIResponses, ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | ||
| 166 | + | {Provider: "github-copilot", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra", API: APIResponses, ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5, PriceTierInputTokens: 272000, PriceInputAbove: 20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25, BaseURL: "https://api.individual.githubcopilot.com"}, | |||
| 166 | {Provider: "github-copilot", ID: "grok-code-fast-1", DisplayName: "Grok Code Fast 1", ContextWindow: 128000, MaxOutput: 64000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | 167 | {Provider: "github-copilot", ID: "grok-code-fast-1", DisplayName: "Grok Code Fast 1", ContextWindow: 128000, MaxOutput: 64000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | ||
| 167 | // ----- google ----- | 168 | // ----- google ----- | ||
| 168 | {Provider: "google", ID: "gemini-3-flash-preview", DisplayName: "Gemini 3 Flash Preview", ContextWindow: 1048576, MaxOutput: 65536, Reasoning: true, PriceInput: 0.5, PriceOutput: 3, PriceCacheRead: 0.05, BaseURL: "https://generativelanguage.googleapis.com/v1beta"}, | 169 | {Provider: "google", ID: "gemini-3-flash-preview", DisplayName: "Gemini 3 Flash Preview", ContextWindow: 1048576, MaxOutput: 65536, Reasoning: true, PriceInput: 0.5, PriceOutput: 3, PriceCacheRead: 0.05, BaseURL: "https://generativelanguage.googleapis.com/v1beta"}, | ||
M packages/provider/extra_models.go +5 -0
| @@ -48,6 +48,11 @@ var supplementCatalog = []Model{ | |||
| 48 | 48 | BaseURL: "https://api.x.ai/v1"}, | |
| 49 | 49 | ||
| 50 | 50 | // ----- openai-responses (public OpenAI Responses API) ----- | |
| 51 | + | {Provider: "openai-responses", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra (Responses)", API: APIResponses, | |
| 52 | + | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | |
| 53 | + | PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5, | |
| 54 | + | PriceTierInputTokens: 272000, | |
| 55 | + | PriceInputAbove: 20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25}, | |
| 51 | 56 | {Provider: "openai-responses", ID: "gpt-5", DisplayName: "GPT-5 (Responses)", | |
| 52 | 57 | ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, | |
| 53 | 58 | PriceInput: 1.25, PriceOutput: 10.00, PriceCacheRead: 0.125}, | |
| @@ -48,6 +48,11 @@ var supplementCatalog = []Model{ | |||||
| 48 | BaseURL: "https://api.x.ai/v1"}, | 48 | BaseURL: "https://api.x.ai/v1"}, | ||
| 49 | 49 | ||||
| 50 | // ----- openai-responses (public OpenAI Responses API) ----- | 50 | // ----- openai-responses (public OpenAI Responses API) ----- | ||
| 51 | + | {Provider: "openai-responses", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra (Responses)", API: APIResponses, | |||
| 52 | + | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | |||
| 53 | + | PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5, | |||
| 54 | + | PriceTierInputTokens: 272000, | |||
| 55 | + | PriceInputAbove: 20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25}, | |||
| 51 | {Provider: "openai-responses", ID: "gpt-5", DisplayName: "GPT-5 (Responses)", | 56 | {Provider: "openai-responses", ID: "gpt-5", DisplayName: "GPT-5 (Responses)", | ||
| 52 | ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, | 57 | ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, | ||
| 53 | PriceInput: 1.25, PriceOutput: 10.00, PriceCacheRead: 0.125}, | 58 | PriceInput: 1.25, PriceOutput: 10.00, PriceCacheRead: 0.125}, | ||
M packages/provider/github_copilot.go +2 -3
| @@ -203,9 +203,8 @@ const copilotDefaultBaseURL = "https://api.individual.githubcopilot.com" | |||
| 203 | 203 | // The pat must be a GitHub Personal Access Token with Copilot access. | |
| 204 | 204 | // | |
| 205 | 205 | // Copilot exposes two wire protocols: most models use Chat Completions | |
| 206 | - | // (/chat/completions), but the newer GPT-5.6 family (sol/terra/luna) is | |
| 207 | - | // only served through the Responses API (/responses) and returns an | |
| 208 | - | // "chat/completions is not available for gpt models" error otherwise. | |
| 206 | + | // (/chat/completions), while newer GPT models use the Responses API | |
| 207 | + | // (/responses), including GPT-5.6 (sol/terra/luna) and GPT-6 Astra. | |
| 209 | 208 | // A model router dispatches each request to the matching wire client | |
| 210 | 209 | // based on the model's catalog API tag. | |
| 211 | 210 | func NewGithubCopilotClient(pat string) Client { | |
| @@ -203,9 +203,8 @@ const copilotDefaultBaseURL = "https://api.individual.githubcopilot.com" | |||||
| 203 | // The pat must be a GitHub Personal Access Token with Copilot access. | 203 | // The pat must be a GitHub Personal Access Token with Copilot access. | ||
| 204 | // | 204 | // | ||
| 205 | // Copilot exposes two wire protocols: most models use Chat Completions | 205 | // Copilot exposes two wire protocols: most models use Chat Completions | ||
| 206 | - | // (/chat/completions), but the newer GPT-5.6 family (sol/terra/luna) is | 206 | + | // (/chat/completions), while newer GPT models use the Responses API |
| 207 | - | // only served through the Responses API (/responses) and returns an | 207 | + | // (/responses), including GPT-5.6 (sol/terra/luna) and GPT-6 Astra. |
| 208 | - | // "chat/completions is not available for gpt models" error otherwise. | |||
| 209 | // A model router dispatches each request to the matching wire client | 208 | // A model router dispatches each request to the matching wire client | ||
| 210 | // based on the model's catalog API tag. | 209 | // based on the model's catalog API tag. | ||
| 211 | func NewGithubCopilotClient(pat string) Client { | 210 | func NewGithubCopilotClient(pat string) Client { | ||
M packages/provider/github_copilot_test.go +2 -2
| @@ -69,7 +69,7 @@ func TestCopilotHostRewrite(t *testing.T) { | |||
| 69 | 69 | } | |
| 70 | 70 | } | |
| 71 | 71 | ||
| 72 | - | func TestCopilotRoutesGPT56ToResponses(t *testing.T) { | |
| 72 | + | func TestCopilotRoutesModernGPTToResponses(t *testing.T) { | |
| 73 | 73 | router, ok := NewGithubCopilotClient("pat").(*modelRouter) | |
| 74 | 74 | if !ok { | |
| 75 | 75 | t.Fatal("NewGithubCopilotClient did not return a modelRouter") | |
| @@ -86,7 +86,7 @@ func TestCopilotRoutesGPT56ToResponses(t *testing.T) { | |||
| 86 | 86 | if codex.baseURL != want { | |
| 87 | 87 | t.Errorf("github-copilot Responses baseURL = %q, want %q", codex.baseURL, want) | |
| 88 | 88 | } | |
| 89 | - | for _, id := range []string{"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"} { | |
| 89 | + | for _, id := range []string{"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra"} { | |
| 90 | 90 | m, err := FindModel("github-copilot", id) | |
| 91 | 91 | if err != nil { | |
| 92 | 92 | t.Fatalf("FindModel(%q): %v", id, err) | |
| @@ -69,7 +69,7 @@ func TestCopilotHostRewrite(t *testing.T) { | |||||
| 69 | } | 69 | } | ||
| 70 | } | 70 | } | ||
| 71 | 71 | ||||
| 72 | - | func TestCopilotRoutesGPT56ToResponses(t *testing.T) { | 72 | + | func TestCopilotRoutesModernGPTToResponses(t *testing.T) { |
| 73 | router, ok := NewGithubCopilotClient("pat").(*modelRouter) | 73 | router, ok := NewGithubCopilotClient("pat").(*modelRouter) | ||
| 74 | if !ok { | 74 | if !ok { | ||
| 75 | t.Fatal("NewGithubCopilotClient did not return a modelRouter") | 75 | t.Fatal("NewGithubCopilotClient did not return a modelRouter") | ||
| @@ -86,7 +86,7 @@ func TestCopilotRoutesGPT56ToResponses(t *testing.T) { | |||||
| 86 | if codex.baseURL != want { | 86 | if codex.baseURL != want { | ||
| 87 | t.Errorf("github-copilot Responses baseURL = %q, want %q", codex.baseURL, want) | 87 | t.Errorf("github-copilot Responses baseURL = %q, want %q", codex.baseURL, want) | ||
| 88 | } | 88 | } | ||
| 89 | - | for _, id := range []string{"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"} { | 89 | + | for _, id := range []string{"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra"} { |
| 90 | m, err := FindModel("github-copilot", id) | 90 | m, err := FindModel("github-copilot", id) | ||
| 91 | if err != nil { | 91 | if err != nil { | ||
| 92 | t.Fatalf("FindModel(%q): %v", id, err) | 92 | t.Fatalf("FindModel(%q): %v", id, err) | ||
A packages/provider/gpt6_astra_test.go +83 -0
| @@ -0,0 +1,83 @@ | |||
| 1 | + | package provider | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "slices" | |
| 6 | + | "testing" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestGPT6AstraCatalog(t *testing.T) { | |
| 10 | + | for _, name := range []string{"openai", "openai-responses", "openai-codex", "github-copilot"} { | |
| 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 | + | wantContext := 1050000 | |
| 17 | + | if name == "openai-codex" { | |
| 18 | + | wantContext = 272000 | |
| 19 | + | } | |
| 20 | + | if m.API != APIResponses || m.ContextWindow != wantContext || m.MaxOutput != 128000 || !m.Reasoning || m.Speculative { | |
| 21 | + | t.Fatalf("unexpected model capabilities: %+v", m) | |
| 22 | + | } | |
| 23 | + | if m.PriceInput != 10 || m.PriceOutput != 50 || m.PriceCacheRead != 1 || m.PriceCacheWrite != 12.5 { | |
| 24 | + | t.Fatalf("unexpected standard prices: %+v", m) | |
| 25 | + | } | |
| 26 | + | if m.PriceTierInputTokens != 272000 || m.PriceInputAbove != 20 || m.PriceOutputAbove != 75 || m.PriceCacheReadAbove != 2 || m.PriceCacheWriteAbove != 25 { | |
| 27 | + | t.Fatalf("unexpected long-context prices: %+v", m) | |
| 28 | + | } | |
| 29 | + | wantLevels := []string{"", "low", "medium", "high", "xhigh", "max"} | |
| 30 | + | if got := AvailableReasoningLevels(m); !slices.Equal(got, wantLevels) { | |
| 31 | + | t.Fatalf("reasoning levels = %q, want %q", got, wantLevels) | |
| 32 | + | } | |
| 33 | + | }) | |
| 34 | + | } | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | func TestGPT6AstraResponsesReasoning(t *testing.T) { | |
| 38 | + | copilot := NewGithubCopilotClient("test-token").(*modelRouter) | |
| 39 | + | clients := map[string]*codexClient{ | |
| 40 | + | "openai": NewOpenAIResponsesNamed("test-token", "", "openai").(*renamedClient).inner.(*codexClient), | |
| 41 | + | "openai-responses": NewOpenAIResponsesNamed("test-token", "", "openai-responses").(*renamedClient).inner.(*codexClient), | |
| 42 | + | "openai-codex": NewOpenAICodex("test-token", "test-account", "").(*codexClient), | |
| 43 | + | "github-copilot": copilot.byAPI[APIResponses].(*renamedClient).inner.(*codexClient), | |
| 44 | + | } | |
| 45 | + | for name, client := range clients { | |
| 46 | + | t.Run(name, func(t *testing.T) { | |
| 47 | + | for _, effort := range []string{"", "low", "medium", "high", "xhigh", "max"} { | |
| 48 | + | wire, err := client.buildRequest(Request{Model: "gpt-6-astra", Reasoning: effort}) | |
| 49 | + | if err != nil { | |
| 50 | + | t.Fatal(err) | |
| 51 | + | } | |
| 52 | + | if wire.Model != "gpt-6-astra" { | |
| 53 | + | t.Fatalf("wire model = %q", wire.Model) | |
| 54 | + | } | |
| 55 | + | if effort == "" { | |
| 56 | + | if wire.Reasoning != nil { | |
| 57 | + | t.Fatalf("unexpected reasoning config: %+v", wire.Reasoning) | |
| 58 | + | } | |
| 59 | + | } else if wire.Reasoning == nil || wire.Reasoning.Effort != effort { | |
| 60 | + | t.Fatalf("reasoning = %+v, want %q", wire.Reasoning, effort) | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | }) | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func TestGPT6AstraCopilotDispatch(t *testing.T) { | |
| 68 | + | router := NewGithubCopilotClient("test-token").(*modelRouter) | |
| 69 | + | completions := &routeCaptureClient{name: "github-copilot"} | |
| 70 | + | responses := &routeCaptureClient{name: "github-copilot"} | |
| 71 | + | router.fallback = completions | |
| 72 | + | router.byAPI[APIResponses] = responses | |
| 73 | + | ||
| 74 | + | stream, err := router.Stream(context.Background(), Request{Model: "gpt-6-astra"}) | |
| 75 | + | if err != nil { | |
| 76 | + | t.Fatal(err) | |
| 77 | + | } | |
| 78 | + | for range stream { | |
| 79 | + | } | |
| 80 | + | if !slices.Equal(responses.models, []string{"gpt-6-astra"}) || len(completions.models) != 0 { | |
| 81 | + | t.Fatalf("Responses models = %v, Completions models = %v", responses.models, completions.models) | |
| 82 | + | } | |
| 83 | + | } | |
| @@ -0,0 +1,83 @@ | |||||
| 1 | + | package provider | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "slices" | |||
| 6 | + | "testing" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestGPT6AstraCatalog(t *testing.T) { | |||
| 10 | + | for _, name := range []string{"openai", "openai-responses", "openai-codex", "github-copilot"} { | |||
| 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 | + | wantContext := 1050000 | |||
| 17 | + | if name == "openai-codex" { | |||
| 18 | + | wantContext = 272000 | |||
| 19 | + | } | |||
| 20 | + | if m.API != APIResponses || m.ContextWindow != wantContext || m.MaxOutput != 128000 || !m.Reasoning || m.Speculative { | |||
| 21 | + | t.Fatalf("unexpected model capabilities: %+v", m) | |||
| 22 | + | } | |||
| 23 | + | if m.PriceInput != 10 || m.PriceOutput != 50 || m.PriceCacheRead != 1 || m.PriceCacheWrite != 12.5 { | |||
| 24 | + | t.Fatalf("unexpected standard prices: %+v", m) | |||
| 25 | + | } | |||
| 26 | + | if m.PriceTierInputTokens != 272000 || m.PriceInputAbove != 20 || m.PriceOutputAbove != 75 || m.PriceCacheReadAbove != 2 || m.PriceCacheWriteAbove != 25 { | |||
| 27 | + | t.Fatalf("unexpected long-context prices: %+v", m) | |||
| 28 | + | } | |||
| 29 | + | wantLevels := []string{"", "low", "medium", "high", "xhigh", "max"} | |||
| 30 | + | if got := AvailableReasoningLevels(m); !slices.Equal(got, wantLevels) { | |||
| 31 | + | t.Fatalf("reasoning levels = %q, want %q", got, wantLevels) | |||
| 32 | + | } | |||
| 33 | + | }) | |||
| 34 | + | } | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | func TestGPT6AstraResponsesReasoning(t *testing.T) { | |||
| 38 | + | copilot := NewGithubCopilotClient("test-token").(*modelRouter) | |||
| 39 | + | clients := map[string]*codexClient{ | |||
| 40 | + | "openai": NewOpenAIResponsesNamed("test-token", "", "openai").(*renamedClient).inner.(*codexClient), | |||
| 41 | + | "openai-responses": NewOpenAIResponsesNamed("test-token", "", "openai-responses").(*renamedClient).inner.(*codexClient), | |||
| 42 | + | "openai-codex": NewOpenAICodex("test-token", "test-account", "").(*codexClient), | |||
| 43 | + | "github-copilot": copilot.byAPI[APIResponses].(*renamedClient).inner.(*codexClient), | |||
| 44 | + | } | |||
| 45 | + | for name, client := range clients { | |||
| 46 | + | t.Run(name, func(t *testing.T) { | |||
| 47 | + | for _, effort := range []string{"", "low", "medium", "high", "xhigh", "max"} { | |||
| 48 | + | wire, err := client.buildRequest(Request{Model: "gpt-6-astra", Reasoning: effort}) | |||
| 49 | + | if err != nil { | |||
| 50 | + | t.Fatal(err) | |||
| 51 | + | } | |||
| 52 | + | if wire.Model != "gpt-6-astra" { | |||
| 53 | + | t.Fatalf("wire model = %q", wire.Model) | |||
| 54 | + | } | |||
| 55 | + | if effort == "" { | |||
| 56 | + | if wire.Reasoning != nil { | |||
| 57 | + | t.Fatalf("unexpected reasoning config: %+v", wire.Reasoning) | |||
| 58 | + | } | |||
| 59 | + | } else if wire.Reasoning == nil || wire.Reasoning.Effort != effort { | |||
| 60 | + | t.Fatalf("reasoning = %+v, want %q", wire.Reasoning, effort) | |||
| 61 | + | } | |||
| 62 | + | } | |||
| 63 | + | }) | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | func TestGPT6AstraCopilotDispatch(t *testing.T) { | |||
| 68 | + | router := NewGithubCopilotClient("test-token").(*modelRouter) | |||
| 69 | + | completions := &routeCaptureClient{name: "github-copilot"} | |||
| 70 | + | responses := &routeCaptureClient{name: "github-copilot"} | |||
| 71 | + | router.fallback = completions | |||
| 72 | + | router.byAPI[APIResponses] = responses | |||
| 73 | + | ||||
| 74 | + | stream, err := router.Stream(context.Background(), Request{Model: "gpt-6-astra"}) | |||
| 75 | + | if err != nil { | |||
| 76 | + | t.Fatal(err) | |||
| 77 | + | } | |||
| 78 | + | for range stream { | |||
| 79 | + | } | |||
| 80 | + | if !slices.Equal(responses.models, []string{"gpt-6-astra"}) || len(completions.models) != 0 { | |||
| 81 | + | t.Fatalf("Responses models = %v, Completions models = %v", responses.models, completions.models) | |||
| 82 | + | } | |||
| 83 | + | } | |||
M packages/provider/models.go +16 -0
| @@ -376,6 +376,15 @@ var Catalog = []Model{ | |||
| 376 | 376 | Speculative: true, | |
| 377 | 377 | }, | |
| 378 | 378 | ||
| 379 | + | // ---- OpenAI / GPT-6 ---- | |
| 380 | + | { | |
| 381 | + | Provider: "openai", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra", API: APIResponses, | |
| 382 | + | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | |
| 383 | + | PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5, | |
| 384 | + | PriceTierInputTokens: 272000, | |
| 385 | + | PriceInputAbove: 20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25, | |
| 386 | + | }, | |
| 387 | + | ||
| 379 | 388 | // ---- OpenAI Codex / ChatGPT subscription backend ---- | |
| 380 | 389 | // Same model ids as the OpenAI family, but routed through the | |
| 381 | 390 | // ChatGPT Codex OAuth backend rather than api.openai.com. | |
| @@ -421,6 +430,13 @@ var Catalog = []Model{ | |||
| 421 | 430 | PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125, | |
| 422 | 431 | Speculative: true, | |
| 423 | 432 | }, | |
| 433 | + | { | |
| 434 | + | Provider: "openai-codex", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra", API: APIResponses, | |
| 435 | + | ContextWindow: 272000, MaxOutput: 128000, Reasoning: true, | |
| 436 | + | PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5, | |
| 437 | + | PriceTierInputTokens: 272000, | |
| 438 | + | PriceInputAbove: 20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25, | |
| 439 | + | }, | |
| 424 | 440 | } | |
| 425 | 441 | ||
| 426 | 442 | // DefaultModel is used when the user does not specify one. | |
| @@ -376,6 +376,15 @@ var Catalog = []Model{ | |||||
| 376 | Speculative: true, | 376 | Speculative: true, | ||
| 377 | }, | 377 | }, | ||
| 378 | 378 | ||||
| 379 | + | // ---- OpenAI / GPT-6 ---- | |||
| 380 | + | { | |||
| 381 | + | Provider: "openai", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra", API: APIResponses, | |||
| 382 | + | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | |||
| 383 | + | PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5, | |||
| 384 | + | PriceTierInputTokens: 272000, | |||
| 385 | + | PriceInputAbove: 20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25, | |||
| 386 | + | }, | |||
| 387 | + | ||||
| 379 | // ---- OpenAI Codex / ChatGPT subscription backend ---- | 388 | // ---- OpenAI Codex / ChatGPT subscription backend ---- | ||
| 380 | // Same model ids as the OpenAI family, but routed through the | 389 | // Same model ids as the OpenAI family, but routed through the | ||
| 381 | // ChatGPT Codex OAuth backend rather than api.openai.com. | 390 | // ChatGPT Codex OAuth backend rather than api.openai.com. | ||
| @@ -421,6 +430,13 @@ var Catalog = []Model{ | |||||
| 421 | PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125, | 430 | PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125, | ||
| 422 | Speculative: true, | 431 | Speculative: true, | ||
| 423 | }, | 432 | }, | ||
| 433 | + | { | |||
| 434 | + | Provider: "openai-codex", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra", API: APIResponses, | |||
| 435 | + | ContextWindow: 272000, MaxOutput: 128000, Reasoning: true, | |||
| 436 | + | PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5, | |||
| 437 | + | PriceTierInputTokens: 272000, | |||
| 438 | + | PriceInputAbove: 20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25, | |||
| 439 | + | }, | |||
| 424 | } | 440 | } | ||
| 425 | 441 | ||||
| 426 | // DefaultModel is used when the user does not specify one. | 442 | // DefaultModel is used when the user does not specify one. | ||
M packages/provider/reasoning.go +8 -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 supportsResponsesMaxEffort(id) { | |
| 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 zot 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 max 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 supportsResponsesMaxEffort(model) { | |
| 265 | 265 | return "max" | |
| 266 | 266 | } | |
| 267 | 267 | return "xhigh" | |
| @@ -269,3 +269,8 @@ func OpenAICodexReasoningEffort(level, model string) string { | |||
| 269 | 269 | return "" | |
| 270 | 270 | } | |
| 271 | 271 | } | |
| 272 | + | ||
| 273 | + | func supportsResponsesMaxEffort(model string) bool { | |
| 274 | + | id := strings.ToLower(model) | |
| 275 | + | return strings.HasPrefix(id, "gpt-5.6-") || id == "gpt-6-astra" | |
| 276 | + | } | |
| @@ -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 supportsResponsesMaxEffort(id) { |
| 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 zot levels onto the Responses API effort | 251 | // OpenAICodexReasoningEffort maps zot 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 max 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 supportsResponsesMaxEffort(model) { |
| 265 | return "max" | 265 | return "max" | ||
| 266 | } | 266 | } | ||
| 267 | return "xhigh" | 267 | return "xhigh" | ||
| @@ -269,3 +269,8 @@ func OpenAICodexReasoningEffort(level, model string) string { | |||||
| 269 | return "" | 269 | return "" | ||
| 270 | } | 270 | } | ||
| 271 | } | 271 | } | ||
| 272 | + | ||||
| 273 | + | func supportsResponsesMaxEffort(model string) bool { | |||
| 274 | + | id := strings.ToLower(model) | |||
| 275 | + | return strings.HasPrefix(id, "gpt-5.6-") || id == "gpt-6-astra" | |||
| 276 | + | } | |||
M packages/provider/reasoning_test.go +5 -0
| @@ -15,6 +15,7 @@ func TestAvailableReasoningLevels(t *testing.T) { | |||
| 15 | 15 | {name: "generic openai", model: Model{Reasoning: true}, want: []string{"", "low", "medium", "high"}}, | |
| 16 | 16 | {name: "responses", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.5"}, want: []string{"", "low", "medium", "high", "xhigh"}}, | |
| 17 | 17 | {name: "responses native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}}, | |
| 18 | + | {name: "astra native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-6-astra"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}}, | |
| 18 | 19 | {name: "adaptive", model: Model{Reasoning: true, AdaptiveThinking: true}, want: []string{"", "low", "medium", "high", "xhigh", "max"}}, | |
| 19 | 20 | {name: "adaptive compatible", model: Model{Reasoning: true, AdaptiveThinkingCompat: true}, want: []string{"", "high"}}, | |
| 20 | 21 | {name: "gemini pro", model: Model{Provider: "google", ID: "gemini-3-pro-preview", Reasoning: true}, want: []string{"", "low", "high"}}, | |
| @@ -63,6 +64,7 @@ func TestClampReasoningForModel(t *testing.T) { | |||
| 63 | 64 | {name: "adaptive minimum", model: Model{Reasoning: true, AdaptiveThinking: true}, level: "minimum", want: "low"}, | |
| 64 | 65 | {name: "gemini pro medium", model: Model{Provider: "google", ID: "gemini-3-pro", Reasoning: true}, level: "medium", want: "high"}, | |
| 65 | 66 | {name: "native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, level: "max", want: "max"}, | |
| 67 | + | {name: "astra native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-6-astra"}, level: "max", want: "max"}, | |
| 66 | 68 | { | |
| 67 | 69 | name: "explicit equivalent", | |
| 68 | 70 | model: Model{Reasoning: true, ReasoningLevelMap: map[string]string{"minimum": "high"}}, | |
| @@ -132,6 +134,9 @@ func TestReasoningEffortMappings(t *testing.T) { | |||
| 132 | 134 | {"xhigh", "gpt-5.6-sol", "high", "xhigh", "xhigh", 32768, "xhigh"}, | |
| 133 | 135 | {"max", "gpt-5.6-sol", "high", "max", "max", 32768, "max"}, | |
| 134 | 136 | {"max", "gpt-5.5", "high", "max", "xhigh", 32768, "max"}, | |
| 137 | + | {"max", "gpt-6-astra", "high", "max", "max", 32768, "max"}, | |
| 138 | + | {"max", "GPT-6-ASTRA", "high", "max", "max", 32768, "max"}, | |
| 139 | + | {"max", "gpt-6-unknown", "high", "max", "xhigh", 32768, "max"}, | |
| 135 | 140 | } | |
| 136 | 141 | for _, tc := range cases { | |
| 137 | 142 | if got := NormalizeReasoning(tc.level); got != tc.normalized { | |
| @@ -15,6 +15,7 @@ func TestAvailableReasoningLevels(t *testing.T) { | |||||
| 15 | {name: "generic openai", model: Model{Reasoning: true}, want: []string{"", "low", "medium", "high"}}, | 15 | {name: "generic openai", model: Model{Reasoning: true}, want: []string{"", "low", "medium", "high"}}, | ||
| 16 | {name: "responses", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.5"}, want: []string{"", "low", "medium", "high", "xhigh"}}, | 16 | {name: "responses", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.5"}, want: []string{"", "low", "medium", "high", "xhigh"}}, | ||
| 17 | {name: "responses native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}}, | 17 | {name: "responses native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}}, | ||
| 18 | + | {name: "astra native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-6-astra"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}}, | |||
| 18 | {name: "adaptive", model: Model{Reasoning: true, AdaptiveThinking: true}, want: []string{"", "low", "medium", "high", "xhigh", "max"}}, | 19 | {name: "adaptive", model: Model{Reasoning: true, AdaptiveThinking: true}, want: []string{"", "low", "medium", "high", "xhigh", "max"}}, | ||
| 19 | {name: "adaptive compatible", model: Model{Reasoning: true, AdaptiveThinkingCompat: true}, want: []string{"", "high"}}, | 20 | {name: "adaptive compatible", model: Model{Reasoning: true, AdaptiveThinkingCompat: true}, want: []string{"", "high"}}, | ||
| 20 | {name: "gemini pro", model: Model{Provider: "google", ID: "gemini-3-pro-preview", Reasoning: true}, want: []string{"", "low", "high"}}, | 21 | {name: "gemini pro", model: Model{Provider: "google", ID: "gemini-3-pro-preview", Reasoning: true}, want: []string{"", "low", "high"}}, | ||
| @@ -63,6 +64,7 @@ func TestClampReasoningForModel(t *testing.T) { | |||||
| 63 | {name: "adaptive minimum", model: Model{Reasoning: true, AdaptiveThinking: true}, level: "minimum", want: "low"}, | 64 | {name: "adaptive minimum", model: Model{Reasoning: true, AdaptiveThinking: true}, level: "minimum", want: "low"}, | ||
| 64 | {name: "gemini pro medium", model: Model{Provider: "google", ID: "gemini-3-pro", Reasoning: true}, level: "medium", want: "high"}, | 65 | {name: "gemini pro medium", model: Model{Provider: "google", ID: "gemini-3-pro", Reasoning: true}, level: "medium", want: "high"}, | ||
| 65 | {name: "native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, level: "max", want: "max"}, | 66 | {name: "native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, level: "max", want: "max"}, | ||
| 67 | + | {name: "astra native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-6-astra"}, level: "max", want: "max"}, | |||
| 66 | { | 68 | { | ||
| 67 | name: "explicit equivalent", | 69 | name: "explicit equivalent", | ||
| 68 | model: Model{Reasoning: true, ReasoningLevelMap: map[string]string{"minimum": "high"}}, | 70 | model: Model{Reasoning: true, ReasoningLevelMap: map[string]string{"minimum": "high"}}, | ||
| @@ -132,6 +134,9 @@ func TestReasoningEffortMappings(t *testing.T) { | |||||
| 132 | {"xhigh", "gpt-5.6-sol", "high", "xhigh", "xhigh", 32768, "xhigh"}, | 134 | {"xhigh", "gpt-5.6-sol", "high", "xhigh", "xhigh", 32768, "xhigh"}, | ||
| 133 | {"max", "gpt-5.6-sol", "high", "max", "max", 32768, "max"}, | 135 | {"max", "gpt-5.6-sol", "high", "max", "max", 32768, "max"}, | ||
| 134 | {"max", "gpt-5.5", "high", "max", "xhigh", 32768, "max"}, | 136 | {"max", "gpt-5.5", "high", "max", "xhigh", 32768, "max"}, | ||
| 137 | + | {"max", "gpt-6-astra", "high", "max", "max", 32768, "max"}, | |||
| 138 | + | {"max", "GPT-6-ASTRA", "high", "max", "max", 32768, "max"}, | |||
| 139 | + | {"max", "gpt-6-unknown", "high", "max", "xhigh", 32768, "max"}, | |||
| 135 | } | 140 | } | ||
| 136 | for _, tc := range cases { | 141 | for _, tc := range cases { | ||
| 137 | if got := NormalizeReasoning(tc.level); got != tc.normalized { | 142 | if got := NormalizeReasoning(tc.level); got != tc.normalized { | ||