Fix GPT-5.6 Responses errors and metadata
- commit
- c6cf9e7e7830d844f531f602a6cabc0f33b1efe1
- author
- patriceckhart · · 2026-07-10 05:20 UTC
- parent
- c18646e
- signature
- unsigned
5 files · +113 -40
M packages/provider/models.go +12 -12
| @@ -318,20 +318,20 @@ var Catalog = []Model{ | |||
| 318 | 318 | }, | |
| 319 | 319 | { | |
| 320 | 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, | |
| 321 | + | ContextWindow: 272000, MaxOutput: 128000, Reasoning: true, | |
| 322 | + | PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, PriceCacheWrite: 1.25, | |
| 323 | 323 | Speculative: true, | |
| 324 | 324 | }, | |
| 325 | 325 | { | |
| 326 | 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, | |
| 327 | + | ContextWindow: 272000, MaxOutput: 128000, Reasoning: true, | |
| 328 | + | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, | |
| 329 | 329 | Speculative: true, | |
| 330 | 330 | }, | |
| 331 | 331 | { | |
| 332 | 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, | |
| 333 | + | ContextWindow: 272000, MaxOutput: 128000, Reasoning: true, | |
| 334 | + | PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125, | |
| 335 | 335 | Speculative: true, | |
| 336 | 336 | }, | |
| 337 | 337 | ||
| @@ -364,20 +364,20 @@ var Catalog = []Model{ | |||
| 364 | 364 | }, | |
| 365 | 365 | { | |
| 366 | 366 | Provider: "openai-codex", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna", | |
| 367 | - | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | |
| 368 | - | PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, | |
| 367 | + | ContextWindow: 372000, MaxOutput: 128000, Reasoning: true, | |
| 368 | + | PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, PriceCacheWrite: 1.25, | |
| 369 | 369 | Speculative: true, | |
| 370 | 370 | }, | |
| 371 | 371 | { | |
| 372 | 372 | Provider: "openai-codex", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol", | |
| 373 | - | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | |
| 374 | - | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, | |
| 373 | + | ContextWindow: 372000, MaxOutput: 128000, Reasoning: true, | |
| 374 | + | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, | |
| 375 | 375 | Speculative: true, | |
| 376 | 376 | }, | |
| 377 | 377 | { | |
| 378 | 378 | Provider: "openai-codex", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra", | |
| 379 | - | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | |
| 380 | - | PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, | |
| 379 | + | ContextWindow: 372000, MaxOutput: 128000, Reasoning: true, | |
| 380 | + | PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125, | |
| 381 | 381 | Speculative: true, | |
| 382 | 382 | }, | |
| 383 | 383 | } | |
| @@ -318,20 +318,20 @@ var Catalog = []Model{ | |||||
| 318 | }, | 318 | }, | ||
| 319 | { | 319 | { | ||
| 320 | Provider: "openai", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna", | 320 | Provider: "openai", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna", | ||
| 321 | - | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | 321 | + | ContextWindow: 272000, MaxOutput: 128000, Reasoning: true, |
| 322 | - | PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, | 322 | + | PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, PriceCacheWrite: 1.25, |
| 323 | Speculative: true, | 323 | Speculative: true, | ||
| 324 | }, | 324 | }, | ||
| 325 | { | 325 | { | ||
| 326 | Provider: "openai", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol", | 326 | Provider: "openai", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol", | ||
| 327 | - | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | 327 | + | ContextWindow: 272000, MaxOutput: 128000, Reasoning: true, |
| 328 | - | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, | 328 | + | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, |
| 329 | Speculative: true, | 329 | Speculative: true, | ||
| 330 | }, | 330 | }, | ||
| 331 | { | 331 | { | ||
| 332 | Provider: "openai", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra", | 332 | Provider: "openai", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra", | ||
| 333 | - | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | 333 | + | ContextWindow: 272000, MaxOutput: 128000, Reasoning: true, |
| 334 | - | PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, | 334 | + | PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125, |
| 335 | Speculative: true, | 335 | Speculative: true, | ||
| 336 | }, | 336 | }, | ||
| 337 | 337 | ||||
| @@ -364,20 +364,20 @@ var Catalog = []Model{ | |||||
| 364 | }, | 364 | }, | ||
| 365 | { | 365 | { | ||
| 366 | Provider: "openai-codex", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna", | 366 | Provider: "openai-codex", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna", | ||
| 367 | - | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | 367 | + | ContextWindow: 372000, MaxOutput: 128000, Reasoning: true, |
| 368 | - | PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, | 368 | + | PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, PriceCacheWrite: 1.25, |
| 369 | Speculative: true, | 369 | Speculative: true, | ||
| 370 | }, | 370 | }, | ||
| 371 | { | 371 | { | ||
| 372 | Provider: "openai-codex", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol", | 372 | Provider: "openai-codex", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol", | ||
| 373 | - | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | 373 | + | ContextWindow: 372000, MaxOutput: 128000, Reasoning: true, |
| 374 | - | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, | 374 | + | PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, |
| 375 | Speculative: true, | 375 | Speculative: true, | ||
| 376 | }, | 376 | }, | ||
| 377 | { | 377 | { | ||
| 378 | Provider: "openai-codex", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra", | 378 | Provider: "openai-codex", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra", | ||
| 379 | - | ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, | 379 | + | ContextWindow: 372000, MaxOutput: 128000, Reasoning: true, |
| 380 | - | PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, | 380 | + | PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125, |
| 381 | Speculative: true, | 381 | Speculative: true, | ||
| 382 | }, | 382 | }, | ||
| 383 | } | 383 | } | ||
M packages/provider/openai_codex.go +29 -15
| @@ -38,11 +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 | - | codexRoutingAllGPT56 bool | |
| 45 | - | http *http.Client | |
| 41 | + | token string | |
| 42 | + | accountID string | |
| 43 | + | baseURL string | |
| 44 | + | errorLabel string | |
| 45 | + | http *http.Client | |
| 46 | 46 | } | |
| 47 | 47 | ||
| 48 | 48 | // NewOpenAICodex creates a client that talks to ChatGPT's Codex endpoint | |
| @@ -53,10 +53,11 @@ func NewOpenAICodex(token, accountID, baseURL string) Client { | |||
| 53 | 53 | baseURL = codexDefaultBaseURL | |
| 54 | 54 | } | |
| 55 | 55 | return &codexClient{ | |
| 56 | - | token: token, | |
| 57 | - | accountID: accountID, | |
| 58 | - | baseURL: strings.TrimRight(baseURL, "/"), | |
| 59 | - | http: &http.Client{Timeout: 0}, | |
| 56 | + | token: token, | |
| 57 | + | accountID: accountID, | |
| 58 | + | baseURL: strings.TrimRight(baseURL, "/"), | |
| 59 | + | errorLabel: "codex", | |
| 60 | + | http: &http.Client{Timeout: 0}, | |
| 60 | 61 | } | |
| 61 | 62 | } | |
| 62 | 63 | ||
| @@ -323,10 +324,6 @@ func usesCodexCLIRouting(model string) bool { | |||
| 323 | 324 | } | |
| 324 | 325 | } | |
| 325 | 326 | ||
| 326 | - | func isGPT56PreviewModel(model string) bool { | |
| 327 | - | return strings.HasPrefix(model, "gpt-5.6-") | |
| 328 | - | } | |
| 329 | - | ||
| 330 | 327 | func newCodexSessionID() string { | |
| 331 | 328 | var b [16]byte | |
| 332 | 329 | if _, err := rand.Read(b[:]); err != nil { | |
| @@ -349,7 +346,7 @@ func (c *codexClient) Stream(ctx context.Context, req Request) (<-chan Event, er | |||
| 349 | 346 | return nil, err | |
| 350 | 347 | } | |
| 351 | 348 | var codexCLISessionID string | |
| 352 | - | if usesCodexCLIRouting(wire.Model) || (c.codexRoutingAllGPT56 && isGPT56PreviewModel(wire.Model)) { | |
| 349 | + | if usesCodexCLIRouting(wire.Model) { | |
| 353 | 350 | codexCLISessionID = newCodexSessionID() | |
| 354 | 351 | wire.PromptCacheKey = codexCLISessionID | |
| 355 | 352 | } | |
| @@ -641,14 +638,31 @@ func (c *codexClient) runStream(ctx context.Context, resp *http.Response, req Re | |||
| 641 | 638 | var p struct { | |
| 642 | 639 | Message string `json:"message"` | |
| 643 | 640 | Code string `json:"code"` | |
| 641 | + | Error struct { | |
| 642 | + | Message string `json:"message"` | |
| 643 | + | Code string `json:"code"` | |
| 644 | + | } `json:"error"` | |
| 644 | 645 | } | |
| 645 | 646 | _ = json.Unmarshal([]byte(ev.Data), &p) | |
| 646 | 647 | msg := p.Message | |
| 648 | + | if msg == "" { | |
| 649 | + | msg = p.Error.Message | |
| 650 | + | } | |
| 647 | 651 | if msg == "" { | |
| 648 | 652 | msg = p.Code | |
| 649 | 653 | } | |
| 654 | + | if msg == "" { | |
| 655 | + | msg = p.Error.Code | |
| 656 | + | } | |
| 657 | + | if msg == "" { | |
| 658 | + | msg = strings.TrimSpace(ev.Data) | |
| 659 | + | } | |
| 650 | 660 | stop = StopError | |
| 651 | - | finalErr = fmt.Errorf("codex error: %s", msg) | |
| 661 | + | label := c.errorLabel | |
| 662 | + | if label == "" { | |
| 663 | + | label = "codex" | |
| 664 | + | } | |
| 665 | + | finalErr = fmt.Errorf("%s error: %s", label, msg) | |
| 652 | 666 | sendDone() | |
| 653 | 667 | return | |
| 654 | 668 | } | |
| @@ -38,11 +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 | - | codexRoutingAllGPT56 bool | 44 | + | errorLabel string |
| 45 | - | http *http.Client | 45 | + | http *http.Client |
| 46 | } | 46 | } | ||
| 47 | 47 | ||||
| 48 | // NewOpenAICodex creates a client that talks to ChatGPT's Codex endpoint | 48 | // NewOpenAICodex creates a client that talks to ChatGPT's Codex endpoint | ||
| @@ -53,10 +53,11 @@ func NewOpenAICodex(token, accountID, baseURL string) Client { | |||||
| 53 | baseURL = codexDefaultBaseURL | 53 | baseURL = codexDefaultBaseURL | ||
| 54 | } | 54 | } | ||
| 55 | return &codexClient{ | 55 | return &codexClient{ | ||
| 56 | - | token: token, | 56 | + | token: token, |
| 57 | - | accountID: accountID, | 57 | + | accountID: accountID, |
| 58 | - | baseURL: strings.TrimRight(baseURL, "/"), | 58 | + | baseURL: strings.TrimRight(baseURL, "/"), |
| 59 | - | http: &http.Client{Timeout: 0}, | 59 | + | errorLabel: "codex", |
| 60 | + | http: &http.Client{Timeout: 0}, | |||
| 60 | } | 61 | } | ||
| 61 | } | 62 | } | ||
| 62 | 63 | ||||
| @@ -323,10 +324,6 @@ func usesCodexCLIRouting(model string) bool { | |||||
| 323 | } | 324 | } | ||
| 324 | } | 325 | } | ||
| 325 | 326 | ||||
| 326 | - | func isGPT56PreviewModel(model string) bool { | |||
| 327 | - | return strings.HasPrefix(model, "gpt-5.6-") | |||
| 328 | - | } | |||
| 329 | - | ||||
| 330 | func newCodexSessionID() string { | 327 | func newCodexSessionID() string { | ||
| 331 | var b [16]byte | 328 | var b [16]byte | ||
| 332 | if _, err := rand.Read(b[:]); err != nil { | 329 | if _, err := rand.Read(b[:]); err != nil { | ||
| @@ -349,7 +346,7 @@ func (c *codexClient) Stream(ctx context.Context, req Request) (<-chan Event, er | |||||
| 349 | return nil, err | 346 | return nil, err | ||
| 350 | } | 347 | } | ||
| 351 | var codexCLISessionID string | 348 | var codexCLISessionID string | ||
| 352 | - | if usesCodexCLIRouting(wire.Model) || (c.codexRoutingAllGPT56 && isGPT56PreviewModel(wire.Model)) { | 349 | + | if usesCodexCLIRouting(wire.Model) { |
| 353 | codexCLISessionID = newCodexSessionID() | 350 | codexCLISessionID = newCodexSessionID() | ||
| 354 | wire.PromptCacheKey = codexCLISessionID | 351 | wire.PromptCacheKey = codexCLISessionID | ||
| 355 | } | 352 | } | ||
| @@ -641,14 +638,31 @@ func (c *codexClient) runStream(ctx context.Context, resp *http.Response, req Re | |||||
| 641 | var p struct { | 638 | var p struct { | ||
| 642 | Message string `json:"message"` | 639 | Message string `json:"message"` | ||
| 643 | Code string `json:"code"` | 640 | Code string `json:"code"` | ||
| 641 | + | Error struct { | |||
| 642 | + | Message string `json:"message"` | |||
| 643 | + | Code string `json:"code"` | |||
| 644 | + | } `json:"error"` | |||
| 644 | } | 645 | } | ||
| 645 | _ = json.Unmarshal([]byte(ev.Data), &p) | 646 | _ = json.Unmarshal([]byte(ev.Data), &p) | ||
| 646 | msg := p.Message | 647 | msg := p.Message | ||
| 648 | + | if msg == "" { | |||
| 649 | + | msg = p.Error.Message | |||
| 650 | + | } | |||
| 647 | if msg == "" { | 651 | if msg == "" { | ||
| 648 | msg = p.Code | 652 | msg = p.Code | ||
| 649 | } | 653 | } | ||
| 654 | + | if msg == "" { | |||
| 655 | + | msg = p.Error.Code | |||
| 656 | + | } | |||
| 657 | + | if msg == "" { | |||
| 658 | + | msg = strings.TrimSpace(ev.Data) | |||
| 659 | + | } | |||
| 650 | stop = StopError | 660 | stop = StopError | ||
| 651 | - | finalErr = fmt.Errorf("codex error: %s", msg) | 661 | + | label := c.errorLabel |
| 662 | + | if label == "" { | |||
| 663 | + | label = "codex" | |||
| 664 | + | } | |||
| 665 | + | finalErr = fmt.Errorf("%s error: %s", label, msg) | |||
| 652 | sendDone() | 666 | sendDone() | ||
| 653 | return | 667 | return | ||
| 654 | } | 668 | } | ||
M packages/provider/openai_codex_test.go +57 -0
| @@ -114,6 +114,63 @@ func TestCodexPreviewModelUsesCodexCLIShape(t *testing.T) { | |||
| 114 | 114 | } | |
| 115 | 115 | } | |
| 116 | 116 | ||
| 117 | + | func TestOpenAIGPT56DoesNotUseCodexCLIRouting(t *testing.T) { | |
| 118 | + | named := NewOpenAIResponsesNamed("token", "https://example.test/v1/responses", "openai").(*renamedClient) | |
| 119 | + | c := named.inner.(*codexClient) | |
| 120 | + | var gotReq *http.Request | |
| 121 | + | var gotBody codexRequest | |
| 122 | + | c.http.Transport = roundTripFunc(func(r *http.Request) (*http.Response, error) { | |
| 123 | + | gotReq = r | |
| 124 | + | if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil { | |
| 125 | + | t.Fatal(err) | |
| 126 | + | } | |
| 127 | + | return &http.Response{ | |
| 128 | + | StatusCode: http.StatusOK, | |
| 129 | + | Header: http.Header{"Content-Type": []string{"text/event-stream"}}, | |
| 130 | + | Body: io.NopCloser(strings.NewReader("")), | |
| 131 | + | }, nil | |
| 132 | + | }) | |
| 133 | + | ||
| 134 | + | events, err := c.Stream(context.Background(), Request{ | |
| 135 | + | Model: "gpt-5.6-sol", | |
| 136 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |
| 137 | + | }) | |
| 138 | + | if err != nil { | |
| 139 | + | t.Fatal(err) | |
| 140 | + | } | |
| 141 | + | for range events { | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | if gotReq == nil { | |
| 145 | + | t.Fatal("request was not sent") | |
| 146 | + | } | |
| 147 | + | if gotReq.Header.Get("session-id") != "" { | |
| 148 | + | t.Fatalf("session-id = %q", gotReq.Header.Get("session-id")) | |
| 149 | + | } | |
| 150 | + | if gotBody.PromptCacheKey != "" { | |
| 151 | + | t.Fatalf("prompt_cache_key = %q", gotBody.PromptCacheKey) | |
| 152 | + | } | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | func TestCodexNestedStreamError(t *testing.T) { | |
| 156 | + | c := NewOpenAICodex("token", "acct", "").(*codexClient) | |
| 157 | + | resp := &http.Response{ | |
| 158 | + | Body: io.NopCloser(strings.NewReader("data: {\"type\":\"error\",\"error\":{\"code\":\"model_not_available\",\"message\":\"limited preview\"}}\n\n")), | |
| 159 | + | } | |
| 160 | + | out := make(chan Event, 16) | |
| 161 | + | go c.runStream(context.Background(), resp, Request{Model: "gpt-5.6-sol"}, out) | |
| 162 | + | ||
| 163 | + | var got error | |
| 164 | + | for ev := range out { | |
| 165 | + | if done, ok := ev.(EventDone); ok { | |
| 166 | + | got = done.Err | |
| 167 | + | } | |
| 168 | + | } | |
| 169 | + | if got == nil || got.Error() != "codex error: limited preview" { | |
| 170 | + | t.Fatalf("error = %v", got) | |
| 171 | + | } | |
| 172 | + | } | |
| 173 | + | ||
| 117 | 174 | func TestCodexSolKeepsZotShape(t *testing.T) { | |
| 118 | 175 | c := NewOpenAICodex("token", "acct", "https://example.test/backend-api/codex/responses").(*codexClient) | |
| 119 | 176 | var gotReq *http.Request | |
| @@ -114,6 +114,63 @@ func TestCodexPreviewModelUsesCodexCLIShape(t *testing.T) { | |||||
| 114 | } | 114 | } | ||
| 115 | } | 115 | } | ||
| 116 | 116 | ||||
| 117 | + | func TestOpenAIGPT56DoesNotUseCodexCLIRouting(t *testing.T) { | |||
| 118 | + | named := NewOpenAIResponsesNamed("token", "https://example.test/v1/responses", "openai").(*renamedClient) | |||
| 119 | + | c := named.inner.(*codexClient) | |||
| 120 | + | var gotReq *http.Request | |||
| 121 | + | var gotBody codexRequest | |||
| 122 | + | c.http.Transport = roundTripFunc(func(r *http.Request) (*http.Response, error) { | |||
| 123 | + | gotReq = r | |||
| 124 | + | if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil { | |||
| 125 | + | t.Fatal(err) | |||
| 126 | + | } | |||
| 127 | + | return &http.Response{ | |||
| 128 | + | StatusCode: http.StatusOK, | |||
| 129 | + | Header: http.Header{"Content-Type": []string{"text/event-stream"}}, | |||
| 130 | + | Body: io.NopCloser(strings.NewReader("")), | |||
| 131 | + | }, nil | |||
| 132 | + | }) | |||
| 133 | + | ||||
| 134 | + | events, err := c.Stream(context.Background(), Request{ | |||
| 135 | + | Model: "gpt-5.6-sol", | |||
| 136 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |||
| 137 | + | }) | |||
| 138 | + | if err != nil { | |||
| 139 | + | t.Fatal(err) | |||
| 140 | + | } | |||
| 141 | + | for range events { | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | if gotReq == nil { | |||
| 145 | + | t.Fatal("request was not sent") | |||
| 146 | + | } | |||
| 147 | + | if gotReq.Header.Get("session-id") != "" { | |||
| 148 | + | t.Fatalf("session-id = %q", gotReq.Header.Get("session-id")) | |||
| 149 | + | } | |||
| 150 | + | if gotBody.PromptCacheKey != "" { | |||
| 151 | + | t.Fatalf("prompt_cache_key = %q", gotBody.PromptCacheKey) | |||
| 152 | + | } | |||
| 153 | + | } | |||
| 154 | + | ||||
| 155 | + | func TestCodexNestedStreamError(t *testing.T) { | |||
| 156 | + | c := NewOpenAICodex("token", "acct", "").(*codexClient) | |||
| 157 | + | resp := &http.Response{ | |||
| 158 | + | Body: io.NopCloser(strings.NewReader("data: {\"type\":\"error\",\"error\":{\"code\":\"model_not_available\",\"message\":\"limited preview\"}}\n\n")), | |||
| 159 | + | } | |||
| 160 | + | out := make(chan Event, 16) | |||
| 161 | + | go c.runStream(context.Background(), resp, Request{Model: "gpt-5.6-sol"}, out) | |||
| 162 | + | ||||
| 163 | + | var got error | |||
| 164 | + | for ev := range out { | |||
| 165 | + | if done, ok := ev.(EventDone); ok { | |||
| 166 | + | got = done.Err | |||
| 167 | + | } | |||
| 168 | + | } | |||
| 169 | + | if got == nil || got.Error() != "codex error: limited preview" { | |||
| 170 | + | t.Fatalf("error = %v", got) | |||
| 171 | + | } | |||
| 172 | + | } | |||
| 173 | + | ||||
| 117 | func TestCodexSolKeepsZotShape(t *testing.T) { | 174 | func TestCodexSolKeepsZotShape(t *testing.T) { | ||
| 118 | c := NewOpenAICodex("token", "acct", "https://example.test/backend-api/codex/responses").(*codexClient) | 175 | c := NewOpenAICodex("token", "acct", "https://example.test/backend-api/codex/responses").(*codexClient) | ||
| 119 | var gotReq *http.Request | 176 | var gotReq *http.Request | ||
M packages/provider/openai_responses.go +5 -5
| @@ -63,11 +63,11 @@ func NewOpenAIResponsesNamed(apiKey, baseURL, name string) Client { | |||
| 63 | 63 | Timeout: 0, | |
| 64 | 64 | } | |
| 65 | 65 | inner := &codexClient{ | |
| 66 | - | token: apiKey, | |
| 67 | - | accountID: "", // unused; transport strips the header | |
| 68 | - | baseURL: strings.TrimRight(baseURL, "/"), | |
| 69 | - | codexRoutingAllGPT56: true, | |
| 70 | - | http: httpClient, | |
| 66 | + | token: apiKey, | |
| 67 | + | accountID: "", // unused; transport strips the header | |
| 68 | + | baseURL: strings.TrimRight(baseURL, "/"), | |
| 69 | + | errorLabel: "openai", | |
| 70 | + | http: httpClient, | |
| 71 | 71 | } | |
| 72 | 72 | return &renamedClient{inner: inner, name: name} | |
| 73 | 73 | } | |
| @@ -63,11 +63,11 @@ func NewOpenAIResponsesNamed(apiKey, baseURL, name string) Client { | |||||
| 63 | Timeout: 0, | 63 | Timeout: 0, | ||
| 64 | } | 64 | } | ||
| 65 | inner := &codexClient{ | 65 | inner := &codexClient{ | ||
| 66 | - | token: apiKey, | 66 | + | token: apiKey, |
| 67 | - | accountID: "", // unused; transport strips the header | 67 | + | accountID: "", // unused; transport strips the header |
| 68 | - | baseURL: strings.TrimRight(baseURL, "/"), | 68 | + | baseURL: strings.TrimRight(baseURL, "/"), |
| 69 | - | codexRoutingAllGPT56: true, | 69 | + | errorLabel: "openai", |
| 70 | - | http: httpClient, | 70 | + | http: httpClient, |
| 71 | } | 71 | } | ||
| 72 | return &renamedClient{inner: inner, name: name} | 72 | return &renamedClient{inner: inner, name: name} | ||
| 73 | } | 73 | } | ||
M packages/provider/provider_test.go +10 -8
| @@ -389,26 +389,28 @@ func TestGPT56CatalogEntries(t *testing.T) { | |||
| 389 | 389 | cases := []struct { | |
| 390 | 390 | provider string | |
| 391 | 391 | id string | |
| 392 | + | context int | |
| 392 | 393 | priceInput float64 | |
| 393 | 394 | priceOut float64 | |
| 394 | 395 | cacheRead float64 | |
| 396 | + | cacheWrite float64 | |
| 395 | 397 | }{ | |
| 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}, | |
| 399 | - | {"openai-codex", "gpt-5.6-luna", 1, 6, 0.1}, | |
| 400 | - | {"openai-codex", "gpt-5.6-sol", 5, 30, 0.5}, | |
| 401 | - | {"openai-codex", "gpt-5.6-terra", 2.5, 15, 0.25}, | |
| 398 | + | {"openai", "gpt-5.6-luna", 272000, 1, 6, 0.1, 1.25}, | |
| 399 | + | {"openai", "gpt-5.6-sol", 272000, 5, 30, 0.5, 6.25}, | |
| 400 | + | {"openai", "gpt-5.6-terra", 272000, 2.5, 15, 0.25, 3.125}, | |
| 401 | + | {"openai-codex", "gpt-5.6-luna", 372000, 1, 6, 0.1, 1.25}, | |
| 402 | + | {"openai-codex", "gpt-5.6-sol", 372000, 5, 30, 0.5, 6.25}, | |
| 403 | + | {"openai-codex", "gpt-5.6-terra", 372000, 2.5, 15, 0.25, 3.125}, | |
| 402 | 404 | } | |
| 403 | 405 | for _, tc := range cases { | |
| 404 | 406 | m, err := FindModel(tc.provider, tc.id) | |
| 405 | 407 | if err != nil { | |
| 406 | 408 | t.Fatal(err) | |
| 407 | 409 | } | |
| 408 | - | if m.ContextWindow != 1050000 || m.MaxOutput != 128000 || !m.Reasoning { | |
| 410 | + | if m.ContextWindow != tc.context || m.MaxOutput != 128000 || !m.Reasoning { | |
| 409 | 411 | t.Fatalf("%s/%s limits: %+v", tc.provider, tc.id, m) | |
| 410 | 412 | } | |
| 411 | - | if m.PriceInput != tc.priceInput || m.PriceOutput != tc.priceOut || m.PriceCacheRead != tc.cacheRead { | |
| 413 | + | if m.PriceInput != tc.priceInput || m.PriceOutput != tc.priceOut || m.PriceCacheRead != tc.cacheRead || m.PriceCacheWrite != tc.cacheWrite { | |
| 412 | 414 | t.Fatalf("%s/%s prices: %+v", tc.provider, tc.id, m) | |
| 413 | 415 | } | |
| 414 | 416 | } | |
| @@ -389,26 +389,28 @@ func TestGPT56CatalogEntries(t *testing.T) { | |||||
| 389 | cases := []struct { | 389 | cases := []struct { | ||
| 390 | provider string | 390 | provider string | ||
| 391 | id string | 391 | id string | ||
| 392 | + | context int | |||
| 392 | priceInput float64 | 393 | priceInput float64 | ||
| 393 | priceOut float64 | 394 | priceOut float64 | ||
| 394 | cacheRead float64 | 395 | cacheRead float64 | ||
| 396 | + | cacheWrite float64 | |||
| 395 | }{ | 397 | }{ | ||
| 396 | - | {"openai", "gpt-5.6-luna", 1, 6, 0.1}, | 398 | + | {"openai", "gpt-5.6-luna", 272000, 1, 6, 0.1, 1.25}, |
| 397 | - | {"openai", "gpt-5.6-sol", 5, 30, 0.5}, | 399 | + | {"openai", "gpt-5.6-sol", 272000, 5, 30, 0.5, 6.25}, |
| 398 | - | {"openai", "gpt-5.6-terra", 2.5, 15, 0.25}, | 400 | + | {"openai", "gpt-5.6-terra", 272000, 2.5, 15, 0.25, 3.125}, |
| 399 | - | {"openai-codex", "gpt-5.6-luna", 1, 6, 0.1}, | 401 | + | {"openai-codex", "gpt-5.6-luna", 372000, 1, 6, 0.1, 1.25}, |
| 400 | - | {"openai-codex", "gpt-5.6-sol", 5, 30, 0.5}, | 402 | + | {"openai-codex", "gpt-5.6-sol", 372000, 5, 30, 0.5, 6.25}, |
| 401 | - | {"openai-codex", "gpt-5.6-terra", 2.5, 15, 0.25}, | 403 | + | {"openai-codex", "gpt-5.6-terra", 372000, 2.5, 15, 0.25, 3.125}, |
| 402 | } | 404 | } | ||
| 403 | for _, tc := range cases { | 405 | for _, tc := range cases { | ||
| 404 | m, err := FindModel(tc.provider, tc.id) | 406 | m, err := FindModel(tc.provider, tc.id) | ||
| 405 | if err != nil { | 407 | if err != nil { | ||
| 406 | t.Fatal(err) | 408 | t.Fatal(err) | ||
| 407 | } | 409 | } | ||
| 408 | - | if m.ContextWindow != 1050000 || m.MaxOutput != 128000 || !m.Reasoning { | 410 | + | if m.ContextWindow != tc.context || m.MaxOutput != 128000 || !m.Reasoning { |
| 409 | t.Fatalf("%s/%s limits: %+v", tc.provider, tc.id, m) | 411 | t.Fatalf("%s/%s limits: %+v", tc.provider, tc.id, m) | ||
| 410 | } | 412 | } | ||
| 411 | - | if m.PriceInput != tc.priceInput || m.PriceOutput != tc.priceOut || m.PriceCacheRead != tc.cacheRead { | 413 | + | if m.PriceInput != tc.priceInput || m.PriceOutput != tc.priceOut || m.PriceCacheRead != tc.cacheRead || m.PriceCacheWrite != tc.cacheWrite { |
| 412 | t.Fatalf("%s/%s prices: %+v", tc.provider, tc.id, m) | 414 | t.Fatalf("%s/%s prices: %+v", tc.provider, tc.id, m) | ||
| 413 | } | 415 | } | ||
| 414 | } | 416 | } | ||