detached 8a4b834

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{
160160 {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"},
161161 {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"},
162162 {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"},
163166 {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"},
164167 // ----- google -----
165168 {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,
181181 return t.inner.RoundTrip(clone)
182182 }
183183
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+
184202 // NewGithubCopilotClient returns a Copilot-pinned OpenAI-compat client.
185203 // 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.
186211 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}
191214 // Initial baseURL is a sane default; copilotRefreshTransport rewrites
192215 // the host on every request based on the freshly-issued token.
193- return &openaiClient{
216+ completions := &openaiClient{
194217 apiKey: pat, // unused at the wire level (transport overrides Auth) but kept for parity
195- baseURL: "https://api.individual.githubcopilot.com",
218+ baseURL: copilotDefaultBaseURL,
196219 chatCompletionsPath: "/chat/completions",
197220 name: "github-copilot",
198- http: httpClient,
221+ http: completionsHTTP,
222+ }
223+
224+ responsesHTTP := &http.Client{
225+ Transport: &copilotResponsesStripTransport{inner: refresh},
226+ Timeout: 0,
199227 }
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+ })
200240 }
@@ -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 rewrites214 // 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 parity217 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) {
6868 t.Errorf("expected Host header (Request.Host) to be %q, got %q", wantHost, mockRT.lastReq.Host)
6969 }
7070 }
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) {
4040 }
4141
4242 func TestGithubCopilotChatCompletionsURL(t *testing.T) {
43- client, ok := NewGithubCopilotClient("test-token").(*openaiClient)
43+ router, ok := NewGithubCopilotClient("test-token").(*modelRouter)
4444 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")
4650 }
4751
4852 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"

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