Route GPT-5.6 OpenAI models via Responses
- commit
- c18646e1d2a391c26d1e1a9f27462fab64bf5968
- author
- patriceckhart · · 2026-07-09 18:58 UTC
- parent
- 9e3f5d2
- signature
- unsigned
- refs
- tag: v0.2.71
5 files · +47 -10
M packages/agent/build.go +3 -0
| @@ -748,6 +748,9 @@ func (r Resolved) NewClient() provider.Client { | |||
| 748 | 748 | case "deepseek": | |
| 749 | 749 | return wrap(provider.NewDeepSeek(r.Credential, r.BaseURL)) | |
| 750 | 750 | case "openai": | |
| 751 | + | if strings.HasPrefix(r.Model, "gpt-5.6-") { | |
| 752 | + | return wrap(provider.NewOpenAIResponsesNamed(r.Credential, r.BaseURL, "openai")) | |
| 753 | + | } | |
| 751 | 754 | return wrap(provider.NewOpenAI(r.Credential, r.BaseURL)) | |
| 752 | 755 | case "openai-codex": | |
| 753 | 756 | inner := wrap(provider.NewOpenAICodex(r.Credential, r.AccountID, r.BaseURL)) | |
| @@ -748,6 +748,9 @@ func (r Resolved) NewClient() provider.Client { | |||||
| 748 | case "deepseek": | 748 | case "deepseek": | ||
| 749 | return wrap(provider.NewDeepSeek(r.Credential, r.BaseURL)) | 749 | return wrap(provider.NewDeepSeek(r.Credential, r.BaseURL)) | ||
| 750 | case "openai": | 750 | case "openai": | ||
| 751 | + | if strings.HasPrefix(r.Model, "gpt-5.6-") { | |||
| 752 | + | return wrap(provider.NewOpenAIResponsesNamed(r.Credential, r.BaseURL, "openai")) | |||
| 753 | + | } | |||
| 751 | return wrap(provider.NewOpenAI(r.Credential, r.BaseURL)) | 754 | return wrap(provider.NewOpenAI(r.Credential, r.BaseURL)) | ||
| 752 | case "openai-codex": | 755 | case "openai-codex": | ||
| 753 | inner := wrap(provider.NewOpenAICodex(r.Credential, r.AccountID, r.BaseURL)) | 756 | inner := wrap(provider.NewOpenAICodex(r.Credential, r.AccountID, r.BaseURL)) | ||
M packages/provider/models.go +18 -0
| @@ -316,6 +316,24 @@ var Catalog = []Model{ | |||
| 316 | 316 | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, | |
| 317 | 317 | Speculative: true, | |
| 318 | 318 | }, | |
| 319 | + | { | |
| 320 | + | Provider: "openai", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna", | |
| 321 | + | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | |
| 322 | + | PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, | |
| 323 | + | Speculative: true, | |
| 324 | + | }, | |
| 325 | + | { | |
| 326 | + | Provider: "openai", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol", | |
| 327 | + | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | |
| 328 | + | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, | |
| 329 | + | Speculative: true, | |
| 330 | + | }, | |
| 331 | + | { | |
| 332 | + | Provider: "openai", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra", | |
| 333 | + | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | |
| 334 | + | PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, | |
| 335 | + | Speculative: true, | |
| 336 | + | }, | |
| 319 | 337 | ||
| 320 | 338 | // ---- OpenAI Codex / ChatGPT subscription backend ---- | |
| 321 | 339 | // Same model ids as the OpenAI family, but routed through the | |
| @@ -316,6 +316,24 @@ var Catalog = []Model{ | |||||
| 316 | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, | 316 | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, | ||
| 317 | Speculative: true, | 317 | Speculative: true, | ||
| 318 | }, | 318 | }, | ||
| 319 | + | { | |||
| 320 | + | Provider: "openai", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna", | |||
| 321 | + | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | |||
| 322 | + | PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, | |||
| 323 | + | Speculative: true, | |||
| 324 | + | }, | |||
| 325 | + | { | |||
| 326 | + | Provider: "openai", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol", | |||
| 327 | + | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | |||
| 328 | + | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, | |||
| 329 | + | Speculative: true, | |||
| 330 | + | }, | |||
| 331 | + | { | |||
| 332 | + | Provider: "openai", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra", | |||
| 333 | + | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | |||
| 334 | + | PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, | |||
| 335 | + | Speculative: true, | |||
| 336 | + | }, | |||
| 319 | 337 | ||||
| 320 | // ---- OpenAI Codex / ChatGPT subscription backend ---- | 338 | // ---- OpenAI Codex / ChatGPT subscription backend ---- | ||
| 321 | // Same model ids as the OpenAI family, but routed through the | 339 | // Same model ids as the OpenAI family, but routed through the | ||
M packages/provider/openai_codex.go +10 -5
| @@ -38,10 +38,11 @@ import ( | |||
| 38 | 38 | const codexDefaultBaseURL = "https://chatgpt.com/backend-api/codex/responses" | |
| 39 | 39 | ||
| 40 | 40 | type codexClient struct { | |
| 41 | - | token string | |
| 42 | - | accountID string | |
| 43 | - | baseURL string | |
| 44 | - | http *http.Client | |
| 41 | + | token string | |
| 42 | + | accountID string | |
| 43 | + | baseURL string | |
| 44 | + | codexRoutingAllGPT56 bool | |
| 45 | + | http *http.Client | |
| 45 | 46 | } | |
| 46 | 47 | ||
| 47 | 48 | // NewOpenAICodex creates a client that talks to ChatGPT's Codex endpoint | |
| @@ -322,6 +323,10 @@ func usesCodexCLIRouting(model string) bool { | |||
| 322 | 323 | } | |
| 323 | 324 | } | |
| 324 | 325 | ||
| 326 | + | func isGPT56PreviewModel(model string) bool { | |
| 327 | + | return strings.HasPrefix(model, "gpt-5.6-") | |
| 328 | + | } | |
| 329 | + | ||
| 325 | 330 | func newCodexSessionID() string { | |
| 326 | 331 | var b [16]byte | |
| 327 | 332 | if _, err := rand.Read(b[:]); err != nil { | |
| @@ -344,7 +349,7 @@ func (c *codexClient) Stream(ctx context.Context, req Request) (<-chan Event, er | |||
| 344 | 349 | return nil, err | |
| 345 | 350 | } | |
| 346 | 351 | var codexCLISessionID string | |
| 347 | - | if usesCodexCLIRouting(wire.Model) { | |
| 352 | + | if usesCodexCLIRouting(wire.Model) || (c.codexRoutingAllGPT56 && isGPT56PreviewModel(wire.Model)) { | |
| 348 | 353 | codexCLISessionID = newCodexSessionID() | |
| 349 | 354 | wire.PromptCacheKey = codexCLISessionID | |
| 350 | 355 | } | |
| @@ -38,10 +38,11 @@ import ( | |||||
| 38 | const codexDefaultBaseURL = "https://chatgpt.com/backend-api/codex/responses" | 38 | const codexDefaultBaseURL = "https://chatgpt.com/backend-api/codex/responses" | ||
| 39 | 39 | ||||
| 40 | type codexClient struct { | 40 | type codexClient struct { | ||
| 41 | - | token string | 41 | + | token string |
| 42 | - | accountID string | 42 | + | accountID string |
| 43 | - | baseURL string | 43 | + | baseURL string |
| 44 | - | http *http.Client | 44 | + | codexRoutingAllGPT56 bool |
| 45 | + | http *http.Client | |||
| 45 | } | 46 | } | ||
| 46 | 47 | ||||
| 47 | // NewOpenAICodex creates a client that talks to ChatGPT's Codex endpoint | 48 | // NewOpenAICodex creates a client that talks to ChatGPT's Codex endpoint | ||
| @@ -322,6 +323,10 @@ func usesCodexCLIRouting(model string) bool { | |||||
| 322 | } | 323 | } | ||
| 323 | } | 324 | } | ||
| 324 | 325 | ||||
| 326 | + | func isGPT56PreviewModel(model string) bool { | |||
| 327 | + | return strings.HasPrefix(model, "gpt-5.6-") | |||
| 328 | + | } | |||
| 329 | + | ||||
| 325 | func newCodexSessionID() string { | 330 | func newCodexSessionID() string { | ||
| 326 | var b [16]byte | 331 | var b [16]byte | ||
| 327 | if _, err := rand.Read(b[:]); err != nil { | 332 | if _, err := rand.Read(b[:]); err != nil { | ||
| @@ -344,7 +349,7 @@ func (c *codexClient) Stream(ctx context.Context, req Request) (<-chan Event, er | |||||
| 344 | return nil, err | 349 | return nil, err | ||
| 345 | } | 350 | } | ||
| 346 | var codexCLISessionID string | 351 | var codexCLISessionID string | ||
| 347 | - | if usesCodexCLIRouting(wire.Model) { | 352 | + | if usesCodexCLIRouting(wire.Model) || (c.codexRoutingAllGPT56 && isGPT56PreviewModel(wire.Model)) { |
| 348 | codexCLISessionID = newCodexSessionID() | 353 | codexCLISessionID = newCodexSessionID() | ||
| 349 | wire.PromptCacheKey = codexCLISessionID | 354 | wire.PromptCacheKey = codexCLISessionID | ||
| 350 | } | 355 | } | ||
M packages/provider/openai_responses.go +13 -5
| @@ -48,6 +48,13 @@ func (t *openaiResponsesTransport) RoundTrip(req *http.Request) (*http.Response, | |||
| 48 | 48 | // | |
| 49 | 49 | // baseURL may be empty; defaults to https://api.openai.com/v1/responses. | |
| 50 | 50 | func NewOpenAIResponses(apiKey, baseURL string) Client { | |
| 51 | + | return NewOpenAIResponsesNamed(apiKey, baseURL, "openai-responses") | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // NewOpenAIResponsesNamed returns a public OpenAI Responses-API client | |
| 55 | + | // reporting the supplied provider name. This lets the `openai` provider | |
| 56 | + | // route Responses-only preview models without changing the visible provider. | |
| 57 | + | func NewOpenAIResponsesNamed(apiKey, baseURL, name string) Client { | |
| 51 | 58 | if baseURL == "" { | |
| 52 | 59 | baseURL = openaiResponsesDefaultBaseURL | |
| 53 | 60 | } | |
| @@ -56,10 +63,11 @@ func NewOpenAIResponses(apiKey, baseURL string) Client { | |||
| 56 | 63 | Timeout: 0, | |
| 57 | 64 | } | |
| 58 | 65 | inner := &codexClient{ | |
| 59 | - | token: apiKey, | |
| 60 | - | accountID: "", // unused; transport strips the header | |
| 61 | - | baseURL: strings.TrimRight(baseURL, "/"), | |
| 62 | - | http: httpClient, | |
| 66 | + | token: apiKey, | |
| 67 | + | accountID: "", // unused; transport strips the header | |
| 68 | + | baseURL: strings.TrimRight(baseURL, "/"), | |
| 69 | + | codexRoutingAllGPT56: true, | |
| 70 | + | http: httpClient, | |
| 63 | 71 | } | |
| 64 | - | return &renamedClient{inner: inner, name: "openai-responses"} | |
| 72 | + | return &renamedClient{inner: inner, name: name} | |
| 65 | 73 | } | |
| @@ -48,6 +48,13 @@ func (t *openaiResponsesTransport) RoundTrip(req *http.Request) (*http.Response, | |||||
| 48 | // | 48 | // | ||
| 49 | // baseURL may be empty; defaults to https://api.openai.com/v1/responses. | 49 | // baseURL may be empty; defaults to https://api.openai.com/v1/responses. | ||
| 50 | func NewOpenAIResponses(apiKey, baseURL string) Client { | 50 | func NewOpenAIResponses(apiKey, baseURL string) Client { | ||
| 51 | + | return NewOpenAIResponsesNamed(apiKey, baseURL, "openai-responses") | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // NewOpenAIResponsesNamed returns a public OpenAI Responses-API client | |||
| 55 | + | // reporting the supplied provider name. This lets the `openai` provider | |||
| 56 | + | // route Responses-only preview models without changing the visible provider. | |||
| 57 | + | func NewOpenAIResponsesNamed(apiKey, baseURL, name string) Client { | |||
| 51 | if baseURL == "" { | 58 | if baseURL == "" { | ||
| 52 | baseURL = openaiResponsesDefaultBaseURL | 59 | baseURL = openaiResponsesDefaultBaseURL | ||
| 53 | } | 60 | } | ||
| @@ -56,10 +63,11 @@ func NewOpenAIResponses(apiKey, baseURL string) Client { | |||||
| 56 | Timeout: 0, | 63 | Timeout: 0, | ||
| 57 | } | 64 | } | ||
| 58 | inner := &codexClient{ | 65 | inner := &codexClient{ | ||
| 59 | - | token: apiKey, | 66 | + | token: apiKey, |
| 60 | - | accountID: "", // unused; transport strips the header | 67 | + | accountID: "", // unused; transport strips the header |
| 61 | - | baseURL: strings.TrimRight(baseURL, "/"), | 68 | + | baseURL: strings.TrimRight(baseURL, "/"), |
| 62 | - | http: httpClient, | 69 | + | codexRoutingAllGPT56: true, |
| 70 | + | http: httpClient, | |||
| 63 | } | 71 | } | ||
| 64 | - | return &renamedClient{inner: inner, name: "openai-responses"} | 72 | + | return &renamedClient{inner: inner, name: name} |
| 65 | } | 73 | } | ||
M packages/provider/provider_test.go +3 -0
| @@ -393,6 +393,9 @@ func TestGPT56CatalogEntries(t *testing.T) { | |||
| 393 | 393 | priceOut float64 | |
| 394 | 394 | cacheRead float64 | |
| 395 | 395 | }{ | |
| 396 | + | {"openai", "gpt-5.6-luna", 1, 6, 0.1}, | |
| 397 | + | {"openai", "gpt-5.6-sol", 5, 30, 0.5}, | |
| 398 | + | {"openai", "gpt-5.6-terra", 2.5, 15, 0.25}, | |
| 396 | 399 | {"openai-codex", "gpt-5.6-luna", 1, 6, 0.1}, | |
| 397 | 400 | {"openai-codex", "gpt-5.6-sol", 5, 30, 0.5}, | |
| 398 | 401 | {"openai-codex", "gpt-5.6-terra", 2.5, 15, 0.25}, | |
| @@ -393,6 +393,9 @@ func TestGPT56CatalogEntries(t *testing.T) { | |||||
| 393 | priceOut float64 | 393 | priceOut float64 | ||
| 394 | cacheRead float64 | 394 | cacheRead float64 | ||
| 395 | }{ | 395 | }{ | ||
| 396 | + | {"openai", "gpt-5.6-luna", 1, 6, 0.1}, | |||
| 397 | + | {"openai", "gpt-5.6-sol", 5, 30, 0.5}, | |||
| 398 | + | {"openai", "gpt-5.6-terra", 2.5, 15, 0.25}, | |||
| 396 | {"openai-codex", "gpt-5.6-luna", 1, 6, 0.1}, | 399 | {"openai-codex", "gpt-5.6-luna", 1, 6, 0.1}, | ||
| 397 | {"openai-codex", "gpt-5.6-sol", 5, 30, 0.5}, | 400 | {"openai-codex", "gpt-5.6-sol", 5, 30, 0.5}, | ||
| 398 | {"openai-codex", "gpt-5.6-terra", 2.5, 15, 0.25}, | 401 | {"openai-codex", "gpt-5.6-terra", 2.5, 15, 0.25}, | ||