Add gpt-5.6-sol, gpt-5.6-terra, and gpt-5.6-luna to Github Copilot.
Mark them as using APIResponses.
- commit
- 8a4b834035178c50174ec00cd9d699bbc5560727
- author
- Dario · · 2026-08-11 01:58 UTC
- committer
- Dario · · 2026-08-11 03:06 UTC
- parent
- be11074
- signature
- unsigned
4 files · +84 -9
M packages/provider/catalog_builtin.go +3 -0
| @@ -160,6 +160,9 @@ var builtinCatalog = []Model{ | |||
| 160 | 160 | {Provider: "github-copilot", ID: "gpt-5.4", DisplayName: "GPT-5.4", ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | |
| 161 | 161 | {Provider: "github-copilot", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 Mini", ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | |
| 162 | 162 | {Provider: "github-copilot", ID: "gpt-5.5", DisplayName: "GPT-5.5", 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"}, | |
| 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"}, | |
| 163 | 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"}, | |
| 164 | 167 | // ----- google ----- | |
| 165 | 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"}, | |
| @@ -160,6 +160,9 @@ var builtinCatalog = []Model{ | |||||
| 160 | {Provider: "github-copilot", ID: "gpt-5.4", DisplayName: "GPT-5.4", ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | 160 | {Provider: "github-copilot", ID: "gpt-5.4", DisplayName: "GPT-5.4", ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | ||
| 161 | {Provider: "github-copilot", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 Mini", ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | 161 | {Provider: "github-copilot", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 Mini", ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | ||
| 162 | {Provider: "github-copilot", ID: "gpt-5.5", DisplayName: "GPT-5.5", ContextWindow: 400000, MaxOutput: 128000, Reasoning: true, PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, BaseURL: "https://api.individual.githubcopilot.com"}, | 162 | {Provider: "github-copilot", ID: "gpt-5.5", DisplayName: "GPT-5.5", 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"}, | |||
| 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"}, | |||
| 163 | {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"}, | 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"}, | ||
| 164 | // ----- google ----- | 167 | // ----- google ----- | ||
| 165 | {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"}, | 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"}, | ||
M packages/provider/github_copilot.go +47 -7
| @@ -181,20 +181,60 @@ func (t *copilotRefreshTransport) RoundTrip(req *http.Request) (*http.Response, | |||
| 181 | 181 | return t.inner.RoundTrip(clone) | |
| 182 | 182 | } | |
| 183 | 183 | ||
| 184 | + | // copilotResponsesStripTransport removes the codex client's ChatGPT | |
| 185 | + | // OAuth identity headers before handing off to copilotRefreshTransport, | |
| 186 | + | // which applies the Copilot token and identity headers. The Copilot | |
| 187 | + | // proxy rejects requests that carry chatgpt-account-id / openai-beta. | |
| 188 | + | type copilotResponsesStripTransport struct { | |
| 189 | + | inner http.RoundTripper | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | func (t *copilotResponsesStripTransport) RoundTrip(req *http.Request) (*http.Response, error) { | |
| 193 | + | clone := req.Clone(req.Context()) | |
| 194 | + | clone.Header.Del("chatgpt-account-id") | |
| 195 | + | clone.Header.Del("openai-beta") | |
| 196 | + | clone.Header.Del("originator") | |
| 197 | + | return t.inner.RoundTrip(clone) | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | const copilotDefaultBaseURL = "https://api.individual.githubcopilot.com" | |
| 201 | + | ||
| 184 | 202 | // NewGithubCopilotClient returns a Copilot-pinned OpenAI-compat client. | |
| 185 | 203 | // The pat must be a GitHub Personal Access Token with Copilot access. | |
| 204 | + | // | |
| 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. | |
| 209 | + | // A model router dispatches each request to the matching wire client | |
| 210 | + | // based on the model's catalog API tag. | |
| 186 | 211 | func NewGithubCopilotClient(pat string) Client { | |
| 187 | - | httpClient := &http.Client{ | |
| 188 | - | Transport: &copilotRefreshTransport{inner: http.DefaultTransport, pat: pat}, | |
| 189 | - | Timeout: 0, | |
| 190 | - | } | |
| 212 | + | refresh := &copilotRefreshTransport{inner: http.DefaultTransport, pat: pat} | |
| 213 | + | completionsHTTP := &http.Client{Transport: refresh, Timeout: 0} | |
| 191 | 214 | // Initial baseURL is a sane default; copilotRefreshTransport rewrites | |
| 192 | 215 | // the host on every request based on the freshly-issued token. | |
| 193 | - | return &openaiClient{ | |
| 216 | + | completions := &openaiClient{ | |
| 194 | 217 | apiKey: pat, // unused at the wire level (transport overrides Auth) but kept for parity | |
| 195 | - | baseURL: "https://api.individual.githubcopilot.com", | |
| 218 | + | baseURL: copilotDefaultBaseURL, | |
| 196 | 219 | chatCompletionsPath: "/chat/completions", | |
| 197 | 220 | name: "github-copilot", | |
| 198 | - | http: httpClient, | |
| 221 | + | http: completionsHTTP, | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | responsesHTTP := &http.Client{ | |
| 225 | + | Transport: &copilotResponsesStripTransport{inner: refresh}, | |
| 226 | + | Timeout: 0, | |
| 199 | 227 | } | |
| 228 | + | responses := &codexClient{ | |
| 229 | + | token: pat, // unused at the wire level (transport overrides Auth) but kept for parity | |
| 230 | + | baseURL: copilotDefaultBaseURL + "/responses", | |
| 231 | + | errorLabel: "github copilot", | |
| 232 | + | providerName: "github-copilot", | |
| 233 | + | disableCLIRouting: true, | |
| 234 | + | http: responsesHTTP, | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | return NewModelRouter("github-copilot", completions, map[string]Client{ | |
| 238 | + | APIResponses: &renamedClient{inner: responses, name: "github-copilot"}, | |
| 239 | + | }) | |
| 200 | 240 | } | |
| @@ -181,20 +181,60 @@ func (t *copilotRefreshTransport) RoundTrip(req *http.Request) (*http.Response, | |||||
| 181 | return t.inner.RoundTrip(clone) | 181 | return t.inner.RoundTrip(clone) | ||
| 182 | } | 182 | } | ||
| 183 | 183 | ||||
| 184 | + | // copilotResponsesStripTransport removes the codex client's ChatGPT | |||
| 185 | + | // OAuth identity headers before handing off to copilotRefreshTransport, | |||
| 186 | + | // which applies the Copilot token and identity headers. The Copilot | |||
| 187 | + | // proxy rejects requests that carry chatgpt-account-id / openai-beta. | |||
| 188 | + | type copilotResponsesStripTransport struct { | |||
| 189 | + | inner http.RoundTripper | |||
| 190 | + | } | |||
| 191 | + | ||||
| 192 | + | func (t *copilotResponsesStripTransport) RoundTrip(req *http.Request) (*http.Response, error) { | |||
| 193 | + | clone := req.Clone(req.Context()) | |||
| 194 | + | clone.Header.Del("chatgpt-account-id") | |||
| 195 | + | clone.Header.Del("openai-beta") | |||
| 196 | + | clone.Header.Del("originator") | |||
| 197 | + | return t.inner.RoundTrip(clone) | |||
| 198 | + | } | |||
| 199 | + | ||||
| 200 | + | const copilotDefaultBaseURL = "https://api.individual.githubcopilot.com" | |||
| 201 | + | ||||
| 184 | // NewGithubCopilotClient returns a Copilot-pinned OpenAI-compat client. | 202 | // NewGithubCopilotClient returns a Copilot-pinned OpenAI-compat client. | ||
| 185 | // 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 | + | // | |||
| 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. | |||
| 209 | + | // A model router dispatches each request to the matching wire client | |||
| 210 | + | // based on the model's catalog API tag. | |||
| 186 | func NewGithubCopilotClient(pat string) Client { | 211 | func NewGithubCopilotClient(pat string) Client { | ||
| 187 | - | httpClient := &http.Client{ | 212 | + | refresh := &copilotRefreshTransport{inner: http.DefaultTransport, pat: pat} |
| 188 | - | Transport: &copilotRefreshTransport{inner: http.DefaultTransport, pat: pat}, | 213 | + | completionsHTTP := &http.Client{Transport: refresh, Timeout: 0} |
| 189 | - | Timeout: 0, | |||
| 190 | - | } | |||
| 191 | // Initial baseURL is a sane default; copilotRefreshTransport rewrites | 214 | // Initial baseURL is a sane default; copilotRefreshTransport rewrites | ||
| 192 | // the host on every request based on the freshly-issued token. | 215 | // the host on every request based on the freshly-issued token. | ||
| 193 | - | return &openaiClient{ | 216 | + | completions := &openaiClient{ |
| 194 | apiKey: pat, // unused at the wire level (transport overrides Auth) but kept for parity | 217 | apiKey: pat, // unused at the wire level (transport overrides Auth) but kept for parity | ||
| 195 | - | baseURL: "https://api.individual.githubcopilot.com", | 218 | + | baseURL: copilotDefaultBaseURL, |
| 196 | chatCompletionsPath: "/chat/completions", | 219 | chatCompletionsPath: "/chat/completions", | ||
| 197 | name: "github-copilot", | 220 | name: "github-copilot", | ||
| 198 | - | http: httpClient, | 221 | + | http: completionsHTTP, |
| 222 | + | } | |||
| 223 | + | ||||
| 224 | + | responsesHTTP := &http.Client{ | |||
| 225 | + | Transport: &copilotResponsesStripTransport{inner: refresh}, | |||
| 226 | + | Timeout: 0, | |||
| 199 | } | 227 | } | ||
| 228 | + | responses := &codexClient{ | |||
| 229 | + | token: pat, // unused at the wire level (transport overrides Auth) but kept for parity | |||
| 230 | + | baseURL: copilotDefaultBaseURL + "/responses", | |||
| 231 | + | errorLabel: "github copilot", | |||
| 232 | + | providerName: "github-copilot", | |||
| 233 | + | disableCLIRouting: true, | |||
| 234 | + | http: responsesHTTP, | |||
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | return NewModelRouter("github-copilot", completions, map[string]Client{ | |||
| 238 | + | APIResponses: &renamedClient{inner: responses, name: "github-copilot"}, | |||
| 239 | + | }) | |||
| 200 | } | 240 | } | ||
M packages/provider/github_copilot_test.go +28 -0
| @@ -68,3 +68,31 @@ func TestCopilotHostRewrite(t *testing.T) { | |||
| 68 | 68 | t.Errorf("expected Host header (Request.Host) to be %q, got %q", wantHost, mockRT.lastReq.Host) | |
| 69 | 69 | } | |
| 70 | 70 | } | |
| 71 | + | ||
| 72 | + | func TestCopilotRoutesGPT56ToResponses(t *testing.T) { | |
| 73 | + | router, ok := NewGithubCopilotClient("pat").(*modelRouter) | |
| 74 | + | if !ok { | |
| 75 | + | t.Fatal("NewGithubCopilotClient did not return a modelRouter") | |
| 76 | + | } | |
| 77 | + | responses, ok := router.byAPI[APIResponses].(*renamedClient) | |
| 78 | + | if !ok { | |
| 79 | + | t.Fatal("github-copilot router has no Responses client") | |
| 80 | + | } | |
| 81 | + | codex, ok := responses.inner.(*codexClient) | |
| 82 | + | if !ok { | |
| 83 | + | t.Fatal("github-copilot Responses client is not a codexClient") | |
| 84 | + | } | |
| 85 | + | const want = "https://api.individual.githubcopilot.com/responses" | |
| 86 | + | if codex.baseURL != want { | |
| 87 | + | t.Errorf("github-copilot Responses baseURL = %q, want %q", codex.baseURL, want) | |
| 88 | + | } | |
| 89 | + | for _, id := range []string{"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"} { | |
| 90 | + | m, err := FindModel("github-copilot", id) | |
| 91 | + | if err != nil { | |
| 92 | + | t.Fatalf("FindModel(%q): %v", id, err) | |
| 93 | + | } | |
| 94 | + | if m.API != APIResponses { | |
| 95 | + | t.Errorf("model %q API = %q, want %q", id, m.API, APIResponses) | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | } | |
| @@ -68,3 +68,31 @@ func TestCopilotHostRewrite(t *testing.T) { | |||||
| 68 | t.Errorf("expected Host header (Request.Host) to be %q, got %q", wantHost, mockRT.lastReq.Host) | 68 | t.Errorf("expected Host header (Request.Host) to be %q, got %q", wantHost, mockRT.lastReq.Host) | ||
| 69 | } | 69 | } | ||
| 70 | } | 70 | } | ||
| 71 | + | ||||
| 72 | + | func TestCopilotRoutesGPT56ToResponses(t *testing.T) { | |||
| 73 | + | router, ok := NewGithubCopilotClient("pat").(*modelRouter) | |||
| 74 | + | if !ok { | |||
| 75 | + | t.Fatal("NewGithubCopilotClient did not return a modelRouter") | |||
| 76 | + | } | |||
| 77 | + | responses, ok := router.byAPI[APIResponses].(*renamedClient) | |||
| 78 | + | if !ok { | |||
| 79 | + | t.Fatal("github-copilot router has no Responses client") | |||
| 80 | + | } | |||
| 81 | + | codex, ok := responses.inner.(*codexClient) | |||
| 82 | + | if !ok { | |||
| 83 | + | t.Fatal("github-copilot Responses client is not a codexClient") | |||
| 84 | + | } | |||
| 85 | + | const want = "https://api.individual.githubcopilot.com/responses" | |||
| 86 | + | if codex.baseURL != want { | |||
| 87 | + | t.Errorf("github-copilot Responses baseURL = %q, want %q", codex.baseURL, want) | |||
| 88 | + | } | |||
| 89 | + | for _, id := range []string{"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"} { | |||
| 90 | + | m, err := FindModel("github-copilot", id) | |||
| 91 | + | if err != nil { | |||
| 92 | + | t.Fatalf("FindModel(%q): %v", id, err) | |||
| 93 | + | } | |||
| 94 | + | if m.API != APIResponses { | |||
| 95 | + | t.Errorf("model %q API = %q, want %q", id, m.API, APIResponses) | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | } | |||
M packages/provider/openai_url_test.go +6 -2
| @@ -40,9 +40,13 @@ func TestChatCompletionsURL(t *testing.T) { | |||
| 40 | 40 | } | |
| 41 | 41 | ||
| 42 | 42 | func TestGithubCopilotChatCompletionsURL(t *testing.T) { | |
| 43 | - | client, ok := NewGithubCopilotClient("test-token").(*openaiClient) | |
| 43 | + | router, ok := NewGithubCopilotClient("test-token").(*modelRouter) | |
| 44 | 44 | if !ok { | |
| 45 | - | t.Fatal("NewGithubCopilotClient did not return an openaiClient") | |
| 45 | + | t.Fatal("NewGithubCopilotClient did not return a modelRouter") | |
| 46 | + | } | |
| 47 | + | client, ok := router.fallback.(*openaiClient) | |
| 48 | + | if !ok { | |
| 49 | + | t.Fatal("GitHub Copilot router fallback is not an openaiClient") | |
| 46 | 50 | } | |
| 47 | 51 | ||
| 48 | 52 | const want = "https://api.individual.githubcopilot.com/chat/completions" | |
| @@ -40,9 +40,13 @@ func TestChatCompletionsURL(t *testing.T) { | |||||
| 40 | } | 40 | } | ||
| 41 | 41 | ||||
| 42 | func TestGithubCopilotChatCompletionsURL(t *testing.T) { | 42 | func TestGithubCopilotChatCompletionsURL(t *testing.T) { | ||
| 43 | - | client, ok := NewGithubCopilotClient("test-token").(*openaiClient) | 43 | + | router, ok := NewGithubCopilotClient("test-token").(*modelRouter) |
| 44 | if !ok { | 44 | if !ok { | ||
| 45 | - | t.Fatal("NewGithubCopilotClient did not return an openaiClient") | 45 | + | t.Fatal("NewGithubCopilotClient did not return a modelRouter") |
| 46 | + | } | |||
| 47 | + | client, ok := router.fallback.(*openaiClient) | |||
| 48 | + | if !ok { | |||
| 49 | + | t.Fatal("GitHub Copilot router fallback is not an openaiClient") | |||
| 46 | } | 50 | } | ||
| 47 | 51 | ||||
| 48 | const want = "https://api.individual.githubcopilot.com/chat/completions" | 52 | const want = "https://api.individual.githubcopilot.com/chat/completions" | ||