detached dbd0dfa

Add GPT-5.6 to Azure and Vercel providers

commit
dbd0dfa9d4f1f2ad863881da93884a7b6be49dcb
author
patriceckhart · · 2026-07-10 05:56 UTC
parent
1fa7669
signature
unsigned
refs
tag: v0.2.73
8 files · +207 -31
M docs/providers.md +7 -3
@@ -162,12 +162,16 @@ zot --provider cloudflare-workers-ai
162162 ```bash
163163 export AZURE_OPENAI_API_KEY=...
164164 export AZURE_OPENAI_BASE_URL=https://your-resource.openai.azure.com
165-export AZURE_OPENAI_API_VERSION=2024-02-01 # optional
165+export AZURE_OPENAI_API_VERSION=v1 # optional, v1 is the default
166166 zot --provider azure-openai-responses
167167 ```
168168
169-If your Azure deployment names differ from zot model IDs, add model overrides
170-in `$ZOT_HOME/models.json`.
169+The provider uses Azure's Responses API. If deployment names differ from zot
170+model IDs, map them without changing the catalog:
171+
172+```bash
173+export AZURE_OPENAI_DEPLOYMENT_NAME_MAP='gpt-5.6-luna=luna-preview,gpt-5.6-sol=sol-preview'
174+```
171175
172176 ## Auth file
173177
@@ -162,12 +162,16 @@ zot --provider cloudflare-workers-ai
162 ```bash162 ```bash
163 export AZURE_OPENAI_API_KEY=...163 export AZURE_OPENAI_API_KEY=...
164 export AZURE_OPENAI_BASE_URL=https://your-resource.openai.azure.com164 export AZURE_OPENAI_BASE_URL=https://your-resource.openai.azure.com
165-export AZURE_OPENAI_API_VERSION=2024-02-01 # optional165+export AZURE_OPENAI_API_VERSION=v1 # optional, v1 is the default
166 zot --provider azure-openai-responses166 zot --provider azure-openai-responses
167 ```167 ```
168 168
169-If your Azure deployment names differ from zot model IDs, add model overrides169+The provider uses Azure's Responses API. If deployment names differ from zot
170-in `$ZOT_HOME/models.json`.170+model IDs, map them without changing the catalog:
171+
172+```bash
173+export AZURE_OPENAI_DEPLOYMENT_NAME_MAP='gpt-5.6-luna=luna-preview,gpt-5.6-sol=sol-preview'
174+```
171 175
172 ## Auth file176 ## Auth file
173 177
M packages/provider/azure_openai.go +90 -1
@@ -30,7 +30,10 @@ import (
3030 "strings"
3131 )
3232
33-const defaultAzureAPIVersion = "2024-10-21"
33+const (
34+ defaultAzureAPIVersion = "2024-10-21"
35+ defaultAzureResponsesAPIVersion = "v1"
36+)
3437
3538 // azureRewriteTransport rewrites every outgoing OpenAI Chat Completions
3639 // URL into the Azure deployment-scoped shape:
@@ -134,3 +137,89 @@ func NewAzureOpenAI(apiKey, baseURL string) Client {
134137 http: httpClient,
135138 }
136139 }
140+
141+// azureResponsesTransport applies Azure API-key authentication and API
142+// versioning to requests using the OpenAI Responses wire format.
143+type azureResponsesTransport struct {
144+ inner http.RoundTripper
145+ apiKey string
146+ apiVersion string
147+}
148+
149+func (t *azureResponsesTransport) RoundTrip(req *http.Request) (*http.Response, error) {
150+ clone := req.Clone(req.Context())
151+ clone.Header.Del("authorization")
152+ clone.Header.Del("chatgpt-account-id")
153+ clone.Header.Del("openai-beta")
154+ clone.Header.Del("originator")
155+ clone.Header.Set("api-key", t.apiKey)
156+ q := clone.URL.Query()
157+ q.Set("api-version", t.apiVersion)
158+ clone.URL.RawQuery = q.Encode()
159+ return t.inner.RoundTrip(clone)
160+}
161+
162+// NewAzureOpenAIResponses creates an Azure OpenAI Responses API client.
163+// The configured model id is used as the deployment name unless overridden
164+// through AZURE_OPENAI_DEPLOYMENT_NAME_MAP (model=deployment pairs).
165+func NewAzureOpenAIResponses(apiKey, baseURL string) Client {
166+ if baseURL == "" {
167+ baseURL = os.Getenv("AZURE_OPENAI_BASE_URL")
168+ if baseURL == "" {
169+ if resource := os.Getenv("AZURE_OPENAI_RESOURCE_NAME"); resource != "" {
170+ baseURL = "https://" + resource + ".openai.azure.com"
171+ }
172+ }
173+ }
174+ if baseURL == "" {
175+ return &unimplementedClient{
176+ name: "azure-openai-responses",
177+ hint: "set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_RESOURCE_NAME (or pass --base-url)",
178+ }
179+ }
180+
181+ endpoint := strings.TrimRight(baseURL, "/")
182+ switch {
183+ case strings.HasSuffix(endpoint, "/openai/v1/responses"):
184+ case strings.HasSuffix(endpoint, "/openai/v1"):
185+ endpoint += "/responses"
186+ case strings.HasSuffix(endpoint, "/openai"):
187+ endpoint += "/v1/responses"
188+ default:
189+ endpoint += "/openai/v1/responses"
190+ }
191+ apiVersion := os.Getenv("AZURE_OPENAI_API_VERSION")
192+ if apiVersion == "" {
193+ apiVersion = defaultAzureResponsesAPIVersion
194+ }
195+ httpClient := &http.Client{
196+ Transport: &azureResponsesTransport{
197+ inner: http.DefaultTransport,
198+ apiKey: apiKey,
199+ apiVersion: apiVersion,
200+ },
201+ Timeout: 0,
202+ }
203+ inner := &codexClient{
204+ token: apiKey,
205+ baseURL: endpoint,
206+ errorLabel: "azure openai",
207+ providerName: "azure-openai-responses",
208+ modelName: azureDeploymentName,
209+ disableCLIRouting: true,
210+ http: httpClient,
211+ }
212+ return &renamedClient{inner: inner, name: "azure-openai-responses"}
213+}
214+
215+func azureDeploymentName(model string) string {
216+ for _, pair := range strings.Split(os.Getenv("AZURE_OPENAI_DEPLOYMENT_NAME_MAP"), ",") {
217+ parts := strings.SplitN(strings.TrimSpace(pair), "=", 2)
218+ if len(parts) == 2 && strings.TrimSpace(parts[0]) == model {
219+ if deployment := strings.TrimSpace(parts[1]); deployment != "" {
220+ return deployment
221+ }
222+ }
223+ }
224+ return model
225+}
@@ -30,7 +30,10 @@ import (
30 "strings"30 "strings"
31 )31 )
32 32
33-const defaultAzureAPIVersion = "2024-10-21"33+const (
34+ defaultAzureAPIVersion = "2024-10-21"
35+ defaultAzureResponsesAPIVersion = "v1"
36+)
34 37
35 // azureRewriteTransport rewrites every outgoing OpenAI Chat Completions38 // azureRewriteTransport rewrites every outgoing OpenAI Chat Completions
36 // URL into the Azure deployment-scoped shape:39 // URL into the Azure deployment-scoped shape:
@@ -134,3 +137,89 @@ func NewAzureOpenAI(apiKey, baseURL string) Client {
134 http: httpClient,137 http: httpClient,
135 }138 }
136 }139 }
140+
141+// azureResponsesTransport applies Azure API-key authentication and API
142+// versioning to requests using the OpenAI Responses wire format.
143+type azureResponsesTransport struct {
144+ inner http.RoundTripper
145+ apiKey string
146+ apiVersion string
147+}
148+
149+func (t *azureResponsesTransport) RoundTrip(req *http.Request) (*http.Response, error) {
150+ clone := req.Clone(req.Context())
151+ clone.Header.Del("authorization")
152+ clone.Header.Del("chatgpt-account-id")
153+ clone.Header.Del("openai-beta")
154+ clone.Header.Del("originator")
155+ clone.Header.Set("api-key", t.apiKey)
156+ q := clone.URL.Query()
157+ q.Set("api-version", t.apiVersion)
158+ clone.URL.RawQuery = q.Encode()
159+ return t.inner.RoundTrip(clone)
160+}
161+
162+// NewAzureOpenAIResponses creates an Azure OpenAI Responses API client.
163+// The configured model id is used as the deployment name unless overridden
164+// through AZURE_OPENAI_DEPLOYMENT_NAME_MAP (model=deployment pairs).
165+func NewAzureOpenAIResponses(apiKey, baseURL string) Client {
166+ if baseURL == "" {
167+ baseURL = os.Getenv("AZURE_OPENAI_BASE_URL")
168+ if baseURL == "" {
169+ if resource := os.Getenv("AZURE_OPENAI_RESOURCE_NAME"); resource != "" {
170+ baseURL = "https://" + resource + ".openai.azure.com"
171+ }
172+ }
173+ }
174+ if baseURL == "" {
175+ return &unimplementedClient{
176+ name: "azure-openai-responses",
177+ hint: "set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_RESOURCE_NAME (or pass --base-url)",
178+ }
179+ }
180+
181+ endpoint := strings.TrimRight(baseURL, "/")
182+ switch {
183+ case strings.HasSuffix(endpoint, "/openai/v1/responses"):
184+ case strings.HasSuffix(endpoint, "/openai/v1"):
185+ endpoint += "/responses"
186+ case strings.HasSuffix(endpoint, "/openai"):
187+ endpoint += "/v1/responses"
188+ default:
189+ endpoint += "/openai/v1/responses"
190+ }
191+ apiVersion := os.Getenv("AZURE_OPENAI_API_VERSION")
192+ if apiVersion == "" {
193+ apiVersion = defaultAzureResponsesAPIVersion
194+ }
195+ httpClient := &http.Client{
196+ Transport: &azureResponsesTransport{
197+ inner: http.DefaultTransport,
198+ apiKey: apiKey,
199+ apiVersion: apiVersion,
200+ },
201+ Timeout: 0,
202+ }
203+ inner := &codexClient{
204+ token: apiKey,
205+ baseURL: endpoint,
206+ errorLabel: "azure openai",
207+ providerName: "azure-openai-responses",
208+ modelName: azureDeploymentName,
209+ disableCLIRouting: true,
210+ http: httpClient,
211+ }
212+ return &renamedClient{inner: inner, name: "azure-openai-responses"}
213+}
214+
215+func azureDeploymentName(model string) string {
216+ for _, pair := range strings.Split(os.Getenv("AZURE_OPENAI_DEPLOYMENT_NAME_MAP"), ",") {
217+ parts := strings.SplitN(strings.TrimSpace(pair), "=", 2)
218+ if len(parts) == 2 && strings.TrimSpace(parts[0]) == model {
219+ if deployment := strings.TrimSpace(parts[1]); deployment != "" {
220+ return deployment
221+ }
222+ }
223+ }
224+ return model
225+}
A packages/provider/azure_openai_test.go +69 -0
@@ -0,0 +1,69 @@
1+package provider
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "io"
7+ "net/http"
8+ "strings"
9+ "testing"
10+)
11+
12+func TestAzureOpenAIResponsesRequestShape(t *testing.T) {
13+ t.Setenv("AZURE_OPENAI_API_VERSION", "v1")
14+ t.Setenv("AZURE_OPENAI_DEPLOYMENT_NAME_MAP", "gpt-5.6-sol=sol-preview")
15+
16+ named := NewAzureOpenAIResponses("azure-key", "https://example.openai.azure.com").(*renamedClient)
17+ c := named.inner.(*codexClient)
18+ transport := c.http.Transport.(*azureResponsesTransport)
19+ var gotReq *http.Request
20+ var gotBody codexRequest
21+ transport.inner = roundTripFunc(func(r *http.Request) (*http.Response, error) {
22+ gotReq = r
23+ if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
24+ t.Fatal(err)
25+ }
26+ return &http.Response{
27+ StatusCode: http.StatusOK,
28+ Header: http.Header{"Content-Type": []string{"text/event-stream"}},
29+ Body: io.NopCloser(strings.NewReader("")),
30+ }, nil
31+ })
32+
33+ events, err := c.Stream(context.Background(), Request{
34+ Model: "gpt-5.6-sol",
35+ Reasoning: "max",
36+ Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}},
37+ })
38+ if err != nil {
39+ t.Fatal(err)
40+ }
41+ var startProvider string
42+ for event := range events {
43+ if start, ok := event.(EventStart); ok {
44+ startProvider = start.Provider
45+ }
46+ }
47+
48+ if startProvider != "azure-openai-responses" {
49+ t.Fatalf("start provider = %q", startProvider)
50+ }
51+ if gotReq == nil {
52+ t.Fatal("request was not sent")
53+ }
54+ if gotReq.URL.String() != "https://example.openai.azure.com/openai/v1/responses?api-version=v1" {
55+ t.Fatalf("url = %q", gotReq.URL.String())
56+ }
57+ if gotReq.Header.Get("api-key") != "azure-key" || gotReq.Header.Get("authorization") != "" {
58+ t.Fatalf("headers = %#v", gotReq.Header)
59+ }
60+ if gotBody.Model != "sol-preview" {
61+ t.Fatalf("model = %q", gotBody.Model)
62+ }
63+ if gotBody.Reasoning == nil || gotBody.Reasoning.Effort != "max" {
64+ t.Fatalf("reasoning = %+v", gotBody.Reasoning)
65+ }
66+ if gotBody.PromptCacheKey != "" {
67+ t.Fatalf("prompt_cache_key = %q", gotBody.PromptCacheKey)
68+ }
69+}
@@ -0,0 +1,69 @@
1+package provider
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "io"
7+ "net/http"
8+ "strings"
9+ "testing"
10+)
11+
12+func TestAzureOpenAIResponsesRequestShape(t *testing.T) {
13+ t.Setenv("AZURE_OPENAI_API_VERSION", "v1")
14+ t.Setenv("AZURE_OPENAI_DEPLOYMENT_NAME_MAP", "gpt-5.6-sol=sol-preview")
15+
16+ named := NewAzureOpenAIResponses("azure-key", "https://example.openai.azure.com").(*renamedClient)
17+ c := named.inner.(*codexClient)
18+ transport := c.http.Transport.(*azureResponsesTransport)
19+ var gotReq *http.Request
20+ var gotBody codexRequest
21+ transport.inner = roundTripFunc(func(r *http.Request) (*http.Response, error) {
22+ gotReq = r
23+ if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
24+ t.Fatal(err)
25+ }
26+ return &http.Response{
27+ StatusCode: http.StatusOK,
28+ Header: http.Header{"Content-Type": []string{"text/event-stream"}},
29+ Body: io.NopCloser(strings.NewReader("")),
30+ }, nil
31+ })
32+
33+ events, err := c.Stream(context.Background(), Request{
34+ Model: "gpt-5.6-sol",
35+ Reasoning: "max",
36+ Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}},
37+ })
38+ if err != nil {
39+ t.Fatal(err)
40+ }
41+ var startProvider string
42+ for event := range events {
43+ if start, ok := event.(EventStart); ok {
44+ startProvider = start.Provider
45+ }
46+ }
47+
48+ if startProvider != "azure-openai-responses" {
49+ t.Fatalf("start provider = %q", startProvider)
50+ }
51+ if gotReq == nil {
52+ t.Fatal("request was not sent")
53+ }
54+ if gotReq.URL.String() != "https://example.openai.azure.com/openai/v1/responses?api-version=v1" {
55+ t.Fatalf("url = %q", gotReq.URL.String())
56+ }
57+ if gotReq.Header.Get("api-key") != "azure-key" || gotReq.Header.Get("authorization") != "" {
58+ t.Fatalf("headers = %#v", gotReq.Header)
59+ }
60+ if gotBody.Model != "sol-preview" {
61+ t.Fatalf("model = %q", gotBody.Model)
62+ }
63+ if gotBody.Reasoning == nil || gotBody.Reasoning.Effort != "max" {
64+ t.Fatalf("reasoning = %+v", gotBody.Reasoning)
65+ }
66+ if gotBody.PromptCacheKey != "" {
67+ t.Fatalf("prompt_cache_key = %q", gotBody.PromptCacheKey)
68+ }
69+}
M packages/provider/catalog_builtin.go +6 -0
@@ -466,6 +466,9 @@ var builtinCatalog = []Model{
466466 {Provider: "vercel-ai-gateway", ID: "openai/gpt-5.4-pro", DisplayName: "GPT 5.4 Pro", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 30, PriceOutput: 180, PriceCacheRead: 0, BaseURL: "https://ai-gateway.vercel.sh"},
467467 {Provider: "vercel-ai-gateway", ID: "openai/gpt-5.5", DisplayName: "GPT 5.5", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, BaseURL: "https://ai-gateway.vercel.sh"},
468468 {Provider: "vercel-ai-gateway", ID: "openai/gpt-5.5-pro", DisplayName: "GPT 5.5 Pro", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 30, PriceOutput: 180, PriceCacheRead: 0, BaseURL: "https://ai-gateway.vercel.sh"},
469+ {Provider: "vercel-ai-gateway", ID: "openai/gpt-5.6-luna", DisplayName: "GPT 5.6 Luna", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, PriceCacheWrite: 1.25, BaseURL: "https://ai-gateway.vercel.sh"},
470+ {Provider: "vercel-ai-gateway", ID: "openai/gpt-5.6-sol", DisplayName: "GPT 5.6 Sol", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://ai-gateway.vercel.sh"},
471+ {Provider: "vercel-ai-gateway", ID: "openai/gpt-5.6-terra", DisplayName: "GPT 5.6 Terra", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125, BaseURL: "https://ai-gateway.vercel.sh"},
469472 {Provider: "vercel-ai-gateway", ID: "openai/gpt-oss-20b", DisplayName: "GPT OSS 20B", ContextWindow: 131072, MaxOutput: 8192, Reasoning: true, PriceInput: 0.05, PriceOutput: 0.2, PriceCacheRead: 0, BaseURL: "https://ai-gateway.vercel.sh"},
470473 {Provider: "vercel-ai-gateway", ID: "openai/gpt-oss-safeguard-20b", DisplayName: "GPT OSS Safeguard 20B", ContextWindow: 131072, MaxOutput: 65536, Reasoning: true, PriceInput: 0.075, PriceOutput: 0.3, PriceCacheRead: 0.037, BaseURL: "https://ai-gateway.vercel.sh"},
471474 {Provider: "vercel-ai-gateway", ID: "openai/o1", DisplayName: "o1", ContextWindow: 200000, MaxOutput: 100000, Reasoning: true, PriceInput: 15, PriceOutput: 60, PriceCacheRead: 7.5, BaseURL: "https://ai-gateway.vercel.sh"},
@@ -622,6 +625,9 @@ var builtinCatalog = []Model{
622625 {Provider: "azure-openai-responses", ID: "gpt-5.4-pro", DisplayName: "GPT-5.4 Pro", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 30, PriceOutput: 180, PriceCacheRead: 0, BaseURL: ""},
623626 {Provider: "azure-openai-responses", ID: "gpt-5.5", DisplayName: "GPT-5.5", ContextWindow: 272000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, BaseURL: ""},
624627 {Provider: "azure-openai-responses", ID: "gpt-5.5-pro", DisplayName: "GPT-5.5 Pro", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 30, PriceOutput: 180, PriceCacheRead: 0, BaseURL: ""},
628+ {Provider: "azure-openai-responses", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, PriceCacheWrite: 1.25, BaseURL: ""},
629+ {Provider: "azure-openai-responses", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: ""},
630+ {Provider: "azure-openai-responses", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125, BaseURL: ""},
625631 {Provider: "azure-openai-responses", ID: "o1", DisplayName: "o1", ContextWindow: 200000, MaxOutput: 100000, Reasoning: true, PriceInput: 15, PriceOutput: 60, PriceCacheRead: 7.5, BaseURL: ""},
626632 {Provider: "azure-openai-responses", ID: "o1-pro", DisplayName: "o1-pro", ContextWindow: 200000, MaxOutput: 100000, Reasoning: true, PriceInput: 150, PriceOutput: 600, PriceCacheRead: 0, BaseURL: ""},
627633 {Provider: "azure-openai-responses", ID: "o3", DisplayName: "o3", ContextWindow: 200000, MaxOutput: 100000, Reasoning: true, PriceInput: 2, PriceOutput: 8, PriceCacheRead: 0.5, BaseURL: ""},
@@ -466,6 +466,9 @@ var builtinCatalog = []Model{
466 {Provider: "vercel-ai-gateway", ID: "openai/gpt-5.4-pro", DisplayName: "GPT 5.4 Pro", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 30, PriceOutput: 180, PriceCacheRead: 0, BaseURL: "https://ai-gateway.vercel.sh"},466 {Provider: "vercel-ai-gateway", ID: "openai/gpt-5.4-pro", DisplayName: "GPT 5.4 Pro", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 30, PriceOutput: 180, PriceCacheRead: 0, BaseURL: "https://ai-gateway.vercel.sh"},
467 {Provider: "vercel-ai-gateway", ID: "openai/gpt-5.5", DisplayName: "GPT 5.5", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, BaseURL: "https://ai-gateway.vercel.sh"},467 {Provider: "vercel-ai-gateway", ID: "openai/gpt-5.5", DisplayName: "GPT 5.5", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, BaseURL: "https://ai-gateway.vercel.sh"},
468 {Provider: "vercel-ai-gateway", ID: "openai/gpt-5.5-pro", DisplayName: "GPT 5.5 Pro", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 30, PriceOutput: 180, PriceCacheRead: 0, BaseURL: "https://ai-gateway.vercel.sh"},468 {Provider: "vercel-ai-gateway", ID: "openai/gpt-5.5-pro", DisplayName: "GPT 5.5 Pro", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 30, PriceOutput: 180, PriceCacheRead: 0, BaseURL: "https://ai-gateway.vercel.sh"},
469+ {Provider: "vercel-ai-gateway", ID: "openai/gpt-5.6-luna", DisplayName: "GPT 5.6 Luna", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, PriceCacheWrite: 1.25, BaseURL: "https://ai-gateway.vercel.sh"},
470+ {Provider: "vercel-ai-gateway", ID: "openai/gpt-5.6-sol", DisplayName: "GPT 5.6 Sol", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://ai-gateway.vercel.sh"},
471+ {Provider: "vercel-ai-gateway", ID: "openai/gpt-5.6-terra", DisplayName: "GPT 5.6 Terra", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125, BaseURL: "https://ai-gateway.vercel.sh"},
469 {Provider: "vercel-ai-gateway", ID: "openai/gpt-oss-20b", DisplayName: "GPT OSS 20B", ContextWindow: 131072, MaxOutput: 8192, Reasoning: true, PriceInput: 0.05, PriceOutput: 0.2, PriceCacheRead: 0, BaseURL: "https://ai-gateway.vercel.sh"},472 {Provider: "vercel-ai-gateway", ID: "openai/gpt-oss-20b", DisplayName: "GPT OSS 20B", ContextWindow: 131072, MaxOutput: 8192, Reasoning: true, PriceInput: 0.05, PriceOutput: 0.2, PriceCacheRead: 0, BaseURL: "https://ai-gateway.vercel.sh"},
470 {Provider: "vercel-ai-gateway", ID: "openai/gpt-oss-safeguard-20b", DisplayName: "GPT OSS Safeguard 20B", ContextWindow: 131072, MaxOutput: 65536, Reasoning: true, PriceInput: 0.075, PriceOutput: 0.3, PriceCacheRead: 0.037, BaseURL: "https://ai-gateway.vercel.sh"},473 {Provider: "vercel-ai-gateway", ID: "openai/gpt-oss-safeguard-20b", DisplayName: "GPT OSS Safeguard 20B", ContextWindow: 131072, MaxOutput: 65536, Reasoning: true, PriceInput: 0.075, PriceOutput: 0.3, PriceCacheRead: 0.037, BaseURL: "https://ai-gateway.vercel.sh"},
471 {Provider: "vercel-ai-gateway", ID: "openai/o1", DisplayName: "o1", ContextWindow: 200000, MaxOutput: 100000, Reasoning: true, PriceInput: 15, PriceOutput: 60, PriceCacheRead: 7.5, BaseURL: "https://ai-gateway.vercel.sh"},474 {Provider: "vercel-ai-gateway", ID: "openai/o1", DisplayName: "o1", ContextWindow: 200000, MaxOutput: 100000, Reasoning: true, PriceInput: 15, PriceOutput: 60, PriceCacheRead: 7.5, BaseURL: "https://ai-gateway.vercel.sh"},
@@ -622,6 +625,9 @@ var builtinCatalog = []Model{
622 {Provider: "azure-openai-responses", ID: "gpt-5.4-pro", DisplayName: "GPT-5.4 Pro", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 30, PriceOutput: 180, PriceCacheRead: 0, BaseURL: ""},625 {Provider: "azure-openai-responses", ID: "gpt-5.4-pro", DisplayName: "GPT-5.4 Pro", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 30, PriceOutput: 180, PriceCacheRead: 0, BaseURL: ""},
623 {Provider: "azure-openai-responses", ID: "gpt-5.5", DisplayName: "GPT-5.5", ContextWindow: 272000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, BaseURL: ""},626 {Provider: "azure-openai-responses", ID: "gpt-5.5", DisplayName: "GPT-5.5", ContextWindow: 272000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, BaseURL: ""},
624 {Provider: "azure-openai-responses", ID: "gpt-5.5-pro", DisplayName: "GPT-5.5 Pro", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 30, PriceOutput: 180, PriceCacheRead: 0, BaseURL: ""},627 {Provider: "azure-openai-responses", ID: "gpt-5.5-pro", DisplayName: "GPT-5.5 Pro", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 30, PriceOutput: 180, PriceCacheRead: 0, BaseURL: ""},
628+ {Provider: "azure-openai-responses", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, PriceCacheWrite: 1.25, BaseURL: ""},
629+ {Provider: "azure-openai-responses", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: ""},
630+ {Provider: "azure-openai-responses", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra", ContextWindow: 1050000, MaxOutput: 128000, Reasoning: true, PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125, BaseURL: ""},
625 {Provider: "azure-openai-responses", ID: "o1", DisplayName: "o1", ContextWindow: 200000, MaxOutput: 100000, Reasoning: true, PriceInput: 15, PriceOutput: 60, PriceCacheRead: 7.5, BaseURL: ""},631 {Provider: "azure-openai-responses", ID: "o1", DisplayName: "o1", ContextWindow: 200000, MaxOutput: 100000, Reasoning: true, PriceInput: 15, PriceOutput: 60, PriceCacheRead: 7.5, BaseURL: ""},
626 {Provider: "azure-openai-responses", ID: "o1-pro", DisplayName: "o1-pro", ContextWindow: 200000, MaxOutput: 100000, Reasoning: true, PriceInput: 150, PriceOutput: 600, PriceCacheRead: 0, BaseURL: ""},632 {Provider: "azure-openai-responses", ID: "o1-pro", DisplayName: "o1-pro", ContextWindow: 200000, MaxOutput: 100000, Reasoning: true, PriceInput: 150, PriceOutput: 600, PriceCacheRead: 0, BaseURL: ""},
627 {Provider: "azure-openai-responses", ID: "o3", DisplayName: "o3", ContextWindow: 200000, MaxOutput: 100000, Reasoning: true, PriceInput: 2, PriceOutput: 8, PriceCacheRead: 0.5, BaseURL: ""},633 {Provider: "azure-openai-responses", ID: "o3", DisplayName: "o3", ContextWindow: 200000, MaxOutput: 100000, Reasoning: true, PriceInput: 2, PriceOutput: 8, PriceCacheRead: 0.5, BaseURL: ""},
M packages/provider/extra_providers.go +0 -10
@@ -253,16 +253,6 @@ func NewGoogleVertex(apiKey, baseURL string) Client {
253253 return NewVertex(apiKey, baseURL)
254254 }
255255
256-// NewAzureOpenAIResponses delegates to the real Azure OpenAI client.
257-// Despite the provider id mentioning "responses", zot uses Azure's
258-// Chat Completions endpoint (older but functionally complete for our
259-// agent loop) to avoid duplicating the full openai-responses wire
260-// client. Models register under provider id `azure-openai-responses`
261-// so user catalogs keep working unchanged.
262-func NewAzureOpenAIResponses(apiKey, baseURL string) Client {
263- return NewAzureOpenAI(apiKey, baseURL)
264-}
265-
266256 // NewMistral returns a Mistral client using their OpenAI-compatible Chat
267257 // Completions endpoint at https://api.mistral.ai/v1. Mistral also offers a
268258 // bespoke "Conversations" API, but the OpenAI-compat endpoint supports
@@ -253,16 +253,6 @@ func NewGoogleVertex(apiKey, baseURL string) Client {
253 return NewVertex(apiKey, baseURL)253 return NewVertex(apiKey, baseURL)
254 }254 }
255 255
256-// NewAzureOpenAIResponses delegates to the real Azure OpenAI client.
257-// Despite the provider id mentioning "responses", zot uses Azure's
258-// Chat Completions endpoint (older but functionally complete for our
259-// agent loop) to avoid duplicating the full openai-responses wire
260-// client. Models register under provider id `azure-openai-responses`
261-// so user catalogs keep working unchanged.
262-func NewAzureOpenAIResponses(apiKey, baseURL string) Client {
263- return NewAzureOpenAI(apiKey, baseURL)
264-}
265-
266 // NewMistral returns a Mistral client using their OpenAI-compatible Chat256 // NewMistral returns a Mistral client using their OpenAI-compatible Chat
267 // Completions endpoint at https://api.mistral.ai/v1. Mistral also offers a257 // Completions endpoint at https://api.mistral.ai/v1. Mistral also offers a
268 // bespoke "Conversations" API, but the OpenAI-compat endpoint supports258 // bespoke "Conversations" API, but the OpenAI-compat endpoint supports
M packages/provider/openai_codex.go +23 -12
@@ -38,11 +38,14 @@ import (
3838 const codexDefaultBaseURL = "https://chatgpt.com/backend-api/codex/responses"
3939
4040 type codexClient struct {
41- token string
42- accountID string
43- baseURL string
44- errorLabel string
45- http *http.Client
41+ token string
42+ accountID string
43+ baseURL string
44+ errorLabel string
45+ providerName string
46+ modelName func(string) string
47+ disableCLIRouting bool
48+ http *http.Client
4649 }
4750
4851 // NewOpenAICodex creates a client that talks to ChatGPT's Codex endpoint
@@ -53,11 +56,12 @@ func NewOpenAICodex(token, accountID, baseURL string) Client {
5356 baseURL = codexDefaultBaseURL
5457 }
5558 return &codexClient{
56- token: token,
57- accountID: accountID,
58- baseURL: strings.TrimRight(baseURL, "/"),
59- errorLabel: "codex",
60- http: &http.Client{Timeout: 0},
59+ token: token,
60+ accountID: accountID,
61+ baseURL: strings.TrimRight(baseURL, "/"),
62+ errorLabel: "codex",
63+ providerName: "openai-codex",
64+ http: &http.Client{Timeout: 0},
6165 }
6266 }
6367
@@ -345,8 +349,11 @@ func (c *codexClient) Stream(ctx context.Context, req Request) (<-chan Event, er
345349 if err != nil {
346350 return nil, err
347351 }
352+ if c.modelName != nil {
353+ wire.Model = c.modelName(wire.Model)
354+ }
348355 var codexCLISessionID string
349- if usesCodexCLIRouting(wire.Model) {
356+ if !c.disableCLIRouting && usesCodexCLIRouting(wire.Model) {
350357 codexCLISessionID = newCodexSessionID()
351358 wire.PromptCacheKey = codexCLISessionID
352359 }
@@ -402,7 +409,11 @@ func (c *codexClient) runStream(ctx context.Context, resp *http.Response, req Re
402409 if model.ID == "" {
403410 model, _ = FindModel("openai", req.Model)
404411 }
405- out <- EventStart{Model: req.Model, Provider: "openai-codex"}
412+ providerName := c.providerName
413+ if providerName == "" {
414+ providerName = "openai-codex"
415+ }
416+ out <- EventStart{Model: req.Model, Provider: providerName}
406417
407418 raw := make(chan sseEvent, 16)
408419 go readSSE(resp.Body, raw)
@@ -38,11 +38,14 @@ 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 string41+ token string
42- accountID string42+ accountID string
43- baseURL string43+ baseURL string
44- errorLabel string44+ errorLabel string
45- http *http.Client45+ providerName string
46+ modelName func(string) string
47+ disableCLIRouting bool
48+ http *http.Client
46 }49 }
47 50
48 // NewOpenAICodex creates a client that talks to ChatGPT's Codex endpoint51 // NewOpenAICodex creates a client that talks to ChatGPT's Codex endpoint
@@ -53,11 +56,12 @@ func NewOpenAICodex(token, accountID, baseURL string) Client {
53 baseURL = codexDefaultBaseURL56 baseURL = codexDefaultBaseURL
54 }57 }
55 return &codexClient{58 return &codexClient{
56- token: token,59+ token: token,
57- accountID: accountID,60+ accountID: accountID,
58- baseURL: strings.TrimRight(baseURL, "/"),61+ baseURL: strings.TrimRight(baseURL, "/"),
59- errorLabel: "codex",62+ errorLabel: "codex",
60- http: &http.Client{Timeout: 0},63+ providerName: "openai-codex",
64+ http: &http.Client{Timeout: 0},
61 }65 }
62 }66 }
63 67
@@ -345,8 +349,11 @@ func (c *codexClient) Stream(ctx context.Context, req Request) (<-chan Event, er
345 if err != nil {349 if err != nil {
346 return nil, err350 return nil, err
347 }351 }
352+ if c.modelName != nil {
353+ wire.Model = c.modelName(wire.Model)
354+ }
348 var codexCLISessionID string355 var codexCLISessionID string
349- if usesCodexCLIRouting(wire.Model) {356+ if !c.disableCLIRouting && usesCodexCLIRouting(wire.Model) {
350 codexCLISessionID = newCodexSessionID()357 codexCLISessionID = newCodexSessionID()
351 wire.PromptCacheKey = codexCLISessionID358 wire.PromptCacheKey = codexCLISessionID
352 }359 }
@@ -402,7 +409,11 @@ func (c *codexClient) runStream(ctx context.Context, resp *http.Response, req Re
402 if model.ID == "" {409 if model.ID == "" {
403 model, _ = FindModel("openai", req.Model)410 model, _ = FindModel("openai", req.Model)
404 }411 }
405- out <- EventStart{Model: req.Model, Provider: "openai-codex"}412+ providerName := c.providerName
413+ if providerName == "" {
414+ providerName = "openai-codex"
415+ }
416+ out <- EventStart{Model: req.Model, Provider: providerName}
406 417
407 raw := make(chan sseEvent, 16)418 raw := make(chan sseEvent, 16)
408 go readSSE(resp.Body, raw)419 go readSSE(resp.Body, raw)
M packages/provider/openai_responses.go +6 -5
@@ -63,11 +63,12 @@ func NewOpenAIResponsesNamed(apiKey, baseURL, name string) Client {
6363 Timeout: 0,
6464 }
6565 inner := &codexClient{
66- token: apiKey,
67- accountID: "", // unused; transport strips the header
68- baseURL: strings.TrimRight(baseURL, "/"),
69- errorLabel: "openai",
70- http: httpClient,
66+ token: apiKey,
67+ accountID: "", // unused; transport strips the header
68+ baseURL: strings.TrimRight(baseURL, "/"),
69+ errorLabel: "openai",
70+ providerName: name,
71+ http: httpClient,
7172 }
7273 return &renamedClient{inner: inner, name: name}
7374 }
@@ -63,11 +63,12 @@ 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 header67+ accountID: "", // unused; transport strips the header
68- baseURL: strings.TrimRight(baseURL, "/"),68+ baseURL: strings.TrimRight(baseURL, "/"),
69- errorLabel: "openai",69+ errorLabel: "openai",
70- http: httpClient,70+ providerName: name,
71+ http: httpClient,
71 }72 }
72 return &renamedClient{inner: inner, name: name}73 return &renamedClient{inner: inner, name: name}
73 }74 }
M packages/provider/provider_test.go +6 -0
@@ -414,6 +414,12 @@ func TestGPT56CatalogEntries(t *testing.T) {
414414 {"openai-codex", "gpt-5.6-luna", 372000, 1, 6, 0.1, 1.25},
415415 {"openai-codex", "gpt-5.6-sol", 372000, 5, 30, 0.5, 6.25},
416416 {"openai-codex", "gpt-5.6-terra", 372000, 2.5, 15, 0.25, 3.125},
417+ {"azure-openai-responses", "gpt-5.6-luna", 1050000, 1, 6, 0.1, 1.25},
418+ {"azure-openai-responses", "gpt-5.6-sol", 1050000, 5, 30, 0.5, 6.25},
419+ {"azure-openai-responses", "gpt-5.6-terra", 1050000, 2.5, 15, 0.25, 3.125},
420+ {"vercel-ai-gateway", "openai/gpt-5.6-luna", 1050000, 1, 6, 0.1, 1.25},
421+ {"vercel-ai-gateway", "openai/gpt-5.6-sol", 1050000, 5, 30, 0.5, 6.25},
422+ {"vercel-ai-gateway", "openai/gpt-5.6-terra", 1050000, 2.5, 15, 0.25, 3.125},
417423 }
418424 for _, tc := range cases {
419425 m, err := FindModel(tc.provider, tc.id)
@@ -414,6 +414,12 @@ func TestGPT56CatalogEntries(t *testing.T) {
414 {"openai-codex", "gpt-5.6-luna", 372000, 1, 6, 0.1, 1.25},414 {"openai-codex", "gpt-5.6-luna", 372000, 1, 6, 0.1, 1.25},
415 {"openai-codex", "gpt-5.6-sol", 372000, 5, 30, 0.5, 6.25},415 {"openai-codex", "gpt-5.6-sol", 372000, 5, 30, 0.5, 6.25},
416 {"openai-codex", "gpt-5.6-terra", 372000, 2.5, 15, 0.25, 3.125},416 {"openai-codex", "gpt-5.6-terra", 372000, 2.5, 15, 0.25, 3.125},
417+ {"azure-openai-responses", "gpt-5.6-luna", 1050000, 1, 6, 0.1, 1.25},
418+ {"azure-openai-responses", "gpt-5.6-sol", 1050000, 5, 30, 0.5, 6.25},
419+ {"azure-openai-responses", "gpt-5.6-terra", 1050000, 2.5, 15, 0.25, 3.125},
420+ {"vercel-ai-gateway", "openai/gpt-5.6-luna", 1050000, 1, 6, 0.1, 1.25},
421+ {"vercel-ai-gateway", "openai/gpt-5.6-sol", 1050000, 5, 30, 0.5, 6.25},
422+ {"vercel-ai-gateway", "openai/gpt-5.6-terra", 1050000, 2.5, 15, 0.25, 3.125},
417 }423 }
418 for _, tc := range cases {424 for _, tc := range cases {
419 m, err := FindModel(tc.provider, tc.id)425 m, err := FindModel(tc.provider, tc.id)

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