Expose OpenAI Codex as separate provider

This commit is contained in:
patriceckhart 2026-05-20 21:24:19 +02:00
parent f9d14252dc
commit 14ffdb65b0
11 changed files with 200 additions and 66 deletions

View file

@ -357,7 +357,7 @@ func PrintHelp(version string) {
row{"zot tg ...", "short alias for telegram-bot"},
)
section("provider and model flags",
row{"--provider", "provider to use (anthropic|openai|kimi|deepseek|google|ollama)"},
row{"--provider", "provider to use (anthropic|openai|openai-codex|kimi|deepseek|google|ollama)"},
row{"--model ID", "model id (see --list-models)"},
row{"--api-key KEY", "api key for this run (env / auth.json fallback)"},
row{"--base-url URL", "override provider api base url"},

View file

@ -132,6 +132,8 @@ func defaultModelForProvider(prov string) string {
switch prov {
case "openai":
return "gpt-5"
case "openai-codex":
return "gpt-5.5"
case "kimi":
return "kimi-for-coding"
case "deepseek":
@ -156,13 +158,16 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
// User-requested provider (explicit > config > default).
provName := firstNonEmpty(args.Provider, cfg.Provider, "anthropic")
if provName != "anthropic" && provName != "openai" && provName != "kimi" && provName != "deepseek" && provName != "google" && provName != "ollama" {
if provName != "anthropic" && provName != "openai" && provName != "openai-codex" && provName != "kimi" && provName != "deepseek" && provName != "google" && provName != "ollama" {
// Unknown provider (maybe removed or renamed). Fall back to
// the first provider that has credentials, or anthropic.
provName = "anthropic"
if _, _, _, err := ResolveCredentialFull("openai", ""); err == nil {
provName = "openai"
}
if _, _, _, err := ResolveCredentialFull("openai-codex", ""); err == nil {
provName = "openai-codex"
}
if _, _, _, err := ResolveCredentialFull("kimi", ""); err == nil {
provName = "kimi"
}
@ -200,7 +205,7 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
// never shows a "not logged in" banner.
userPickedProvider := args.Provider != ""
if credErr != nil && !userPickedProvider && provName != "ollama" {
for _, other := range []string{"anthropic", "openai", "kimi", "deepseek", "google"} {
for _, other := range []string{"anthropic", "openai", "openai-codex", "kimi", "deepseek", "google"} {
if other == provName {
continue
}
@ -217,6 +222,8 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
switch provName {
case "openai":
model = "gpt-5"
case "openai-codex":
model = "gpt-5.5"
case "kimi":
model = "kimi-for-coding"
case "deepseek":
@ -236,6 +243,8 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
switch provName {
case "openai":
model = "gpt-5"
case "openai-codex":
model = "gpt-5.5"
case "kimi":
model = "kimi-for-coding"
case "deepseek":
@ -521,11 +530,10 @@ func (r Resolved) NewClient() provider.Client {
case "deepseek":
return provider.NewDeepSeek(r.Credential, r.BaseURL)
case "openai":
if r.AuthMethod == "oauth" {
inner := provider.NewOpenAICodex(r.Credential, r.AccountID, r.BaseURL)
return r.wrapWithRefresh(inner)
}
return provider.NewOpenAI(r.Credential, r.BaseURL)
case "openai-codex":
inner := provider.NewOpenAICodex(r.Credential, r.AccountID, r.BaseURL)
return r.wrapWithRefresh(inner)
case "google":
// API-key only path. Gemini Generative Language API.
return provider.NewGemini(r.Credential, r.BaseURL)
@ -543,11 +551,15 @@ func (r Resolved) NewClient() provider.Client {
// sessions (hours) silently fail when the 1-hour token expires.
func (r Resolved) wrapWithRefresh(inner provider.Client) provider.Client {
provName := r.Provider
tokenProvider := provName
if provName == "openai-codex" {
tokenProvider = "openai"
}
baseURL := r.BaseURL
accountID := r.AccountID
refreshFn := func(ctx context.Context) (string, error) {
tok, err := refreshIfExpired(provName, loadOAuthToken(provName))
tok, err := refreshIfExpired(tokenProvider, loadOAuthToken(tokenProvider))
if err != nil {
return "", err
}
@ -556,7 +568,7 @@ func (r Resolved) wrapWithRefresh(inner provider.Client) provider.Client {
factory := func(token string) provider.Client {
switch provName {
case "openai":
case "openai-codex":
return provider.NewOpenAICodex(token, accountID, baseURL)
case "kimi":
return provider.NewKimiWithHeaders(token, baseURL, kimiCodeHeaders())
@ -671,7 +683,7 @@ func kimiCodeHeaders() map[string]string {
func envVarName(provider string) string {
switch provider {
case "openai":
case "openai", "openai-codex":
return "OPENAI"
case "kimi":
return "KIMI"

View file

@ -902,7 +902,7 @@ func runInteractive(ctx context.Context, args Args, version string) error {
BuildAgentForRescue: buildAgentForRescue,
LoggedInProviders: func() []string {
var out []string
for _, p := range []string{"anthropic", "openai", "kimi", "deepseek", "google"} {
for _, p := range []string{"anthropic", "openai", "openai-codex", "kimi", "deepseek", "google"} {
if _, _, err := ResolveCredential(p, ""); err == nil {
out = append(out, p)
}

View file

@ -141,6 +141,10 @@ func ResolveCredentialFull(provider, explicit string) (cred, method, accountID s
if v := os.Getenv("OPENAI_API_KEY"); v != "" {
return v, "apikey", "", nil
}
case "openai-codex":
// ChatGPT/Codex subscription route. It intentionally ignores
// OPENAI_API_KEY so users can keep both OpenAI API and Codex
// subscription credentials configured and choose by provider.
case "kimi":
if v := os.Getenv("KIMI_API_KEY"); v != "" {
return v, "apikey", "", nil
@ -180,6 +184,7 @@ func ResolveCredentialFull(provider, explicit string) (cred, method, accountID s
if c.OpenAI.APIKey != "" {
return c.OpenAI.APIKey, "apikey", "", nil
}
case "openai-codex":
if c.OpenAI.OAuth != nil && c.OpenAI.OAuth.AccessToken != "" {
tok, _ := refreshIfExpired("openai", c.OpenAI.OAuth)
return tok.AccessToken, "oauth", tok.AccountID, nil

View file

@ -2907,7 +2907,7 @@ func (i *Interactive) openLogoutDialog() {
}
var items []logoutItem
for _, p := range []string{"anthropic", "openai", "kimi", "google"} {
for _, p := range []string{"anthropic", "kimi", "google"} {
if creds.Has(p) {
method := creds.Method(p)
if method == "oauth" {
@ -2920,6 +2920,12 @@ func (i *Interactive) openLogoutDialog() {
})
}
}
if creds.OpenAI.APIKey != "" {
items = append(items, logoutItem{label: providerLabel("openai"), target: "openai", method: "api key"})
}
if creds.OpenAI.OAuth != nil {
items = append(items, logoutItem{label: providerLabel("openai-codex"), target: "openai-codex", method: "subscription"})
}
if len(items) == 0 {
i.mu.Lock()
i.statusOK = "no credentials stored; already logged out"
@ -2960,12 +2966,12 @@ func (i *Interactive) doLogout(target string) {
var providers []string
switch target {
case "", "all":
providers = []string{"anthropic", "openai", "kimi", "google"}
case "anthropic", "openai", "kimi", "google":
providers = []string{"anthropic", "openai", "openai-codex", "kimi", "google"}
case "anthropic", "openai", "openai-codex", "kimi", "google":
providers = []string{target}
default:
i.mu.Lock()
i.statusErr = "unknown provider: " + target + " (use anthropic, openai, kimi, google, or all)"
i.statusErr = "unknown provider: " + target + " (use anthropic, openai, openai-codex, kimi, google, or all)"
i.mu.Unlock()
return
}
@ -2973,7 +2979,16 @@ func (i *Interactive) doLogout(target string) {
var errs []string
clearedCurrent := false
for _, p := range providers {
if err := store.Clear(p); err != nil {
var err error
switch p {
case "openai":
err = store.ClearAPIKey("openai")
case "openai-codex":
err = store.ClearOAuth("openai")
default:
err = store.Clear(p)
}
if err != nil {
errs = append(errs, p+": "+err.Error())
continue
}

View file

@ -28,7 +28,7 @@ const (
type loginDialog struct {
step loginStep
method string // "apikey" | "oauth"
provider string // "anthropic" | "openai" | "kimi" | "google"
provider string // "anthropic" | "openai" | "openai-codex" | "kimi" | "google"
message string
success bool
url string
@ -39,8 +39,8 @@ type loginDialog struct {
// provider, captured when Open() runs. Rendered above the
// method picker so the user can see whether they're already
// logged in (and how) before starting a new flow. Keys:
// "anthropic", "openai", "kimi", "google". Value is "apikey", "oauth", or ""
// (not logged in).
// "anthropic", "openai", "openai-codex", "kimi", "google". Value is
// "apikey", "oauth", or "" (not logged in).
status map[string]string
}
@ -65,7 +65,7 @@ func (d *loginDialog) Open(zotHome string) {
d.success = false
d.url = ""
d.cursor = 0
d.status = map[string]string{"anthropic": "", "openai": "", "kimi": "", "deepseek": "", "google": ""}
d.status = map[string]string{"anthropic": "", "openai": "", "openai-codex": "", "kimi": "", "deepseek": "", "google": ""}
// Best-effort: if the auth file can't be read, treat every
// provider as not-logged-in. The status line just won't show
// anything useful in that case, which is fine — the user
@ -73,7 +73,14 @@ func (d *loginDialog) Open(zotHome string) {
path := filepath.Join(zotHome, "auth.json")
if creds, err := auth.NewStore(path).Load(); err == nil {
d.status["anthropic"] = creds.Method("anthropic")
d.status["openai"] = creds.Method("openai")
d.status["openai"] = ""
if creds.OpenAI.APIKey != "" {
d.status["openai"] = "apikey"
}
d.status["openai-codex"] = ""
if creds.OpenAI.OAuth != nil {
d.status["openai-codex"] = "oauth"
}
d.status["kimi"] = creds.Method("kimi")
d.status["deepseek"] = creds.Method("deepseek")
d.status["google"] = creds.Method("google")
@ -205,7 +212,7 @@ func (d *loginDialog) Render(th tui.Theme, width int) []string {
// subscription product at all).
func providersForMethod(method string) []string {
if method == "oauth" {
return []string{"anthropic", "openai", "kimi"}
return []string{"anthropic", "openai-codex", "kimi"}
}
return []string{"anthropic", "openai", "kimi", "deepseek", "google"}
}
@ -216,7 +223,9 @@ func providerLabel(id string) string {
case "anthropic":
return "Anthropic (Claude Pro/Max)"
case "openai":
return "OpenAI (ChatGPT Plus/Pro)"
return "OpenAI"
case "openai-codex":
return "OpenAI Codex (ChatGPT Plus/Pro)"
case "kimi":
return "Kimi Code"
case "deepseek":
@ -240,10 +249,11 @@ func providerLabel(id string) string {
func (d *loginDialog) renderStatusLines(th tui.Theme) []string {
anth := d.status["anthropic"]
op := d.status["openai"]
codex := d.status["openai-codex"]
kimi := d.status["kimi"]
ds := d.status["deepseek"]
goog := d.status["google"]
if anth == "" && op == "" && kimi == "" && ds == "" && goog == "" {
if anth == "" && op == "" && codex == "" && kimi == "" && ds == "" && goog == "" {
return nil
}
row := func(id, method string) string {
@ -265,6 +275,7 @@ func (d *loginDialog) renderStatusLines(th tui.Theme) []string {
return []string{
row("anthropic", anth),
row("openai", op),
row("openai-codex", codex),
row("kimi", kimi),
row("deepseek", ds),
row("google", goog),

View file

@ -34,9 +34,11 @@ type Manager struct {
oauthCtx context.Context
oauthCancel context.CancelFunc
manualOp *OAuthProvider
manualPKCE PKCE
manualState string
manualOp *OAuthProvider
manualStoreProvider string
manualEventProvider string
manualPKCE PKCE
manualState string
}
// NewManager returns a Manager bound to store.
@ -133,18 +135,20 @@ func (m *Manager) StartOAuth(provider string) (string, error) {
if provider == "kimi" {
return m.StartKimiDeviceOAuth()
}
storeProvider := provider
var op OAuthProvider
switch provider {
case "anthropic":
op = AnthropicOAuth
case "openai":
case "openai", "openai-codex":
op = OpenAIOAuth
storeProvider = "openai"
case "google":
return "", fmt.Errorf("google login is api-key only; use api key login for gemini")
case "deepseek":
return "", fmt.Errorf("deepseek login is api-key only; use api key login")
default:
return "", fmt.Errorf("provider must be anthropic, openai, kimi, deepseek, or google")
return "", fmt.Errorf("provider must be anthropic, openai, openai-codex, kimi, deepseek, or google")
}
m.mu.Lock()
@ -178,13 +182,13 @@ func (m *Manager) StartOAuth(provider string) (string, error) {
m.oauthCancel = cancel
m.mu.Unlock()
go m.awaitOAuth(ctx, op, cs, pkce, state)
go m.awaitOAuth(ctx, op, storeProvider, provider, cs, pkce, state)
go m.maybeOpen(authURL)
m.emit(Event{Kind: "started", Provider: provider, Method: "oauth", URL: authURL})
return authURL, nil
}
func (m *Manager) awaitOAuth(ctx context.Context, op OAuthProvider, cs *CallbackServer, pkce PKCE, state string) {
func (m *Manager) awaitOAuth(ctx context.Context, op OAuthProvider, storeProvider, eventProvider string, cs *CallbackServer, pkce PKCE, state string) {
defer cs.Shutdown()
waitCtx, waitCancel := context.WithTimeout(ctx, 10*time.Minute)
@ -192,12 +196,12 @@ func (m *Manager) awaitOAuth(ctx context.Context, op OAuthProvider, cs *Callback
res, err := cs.Result(waitCtx)
if err != nil {
if ctx.Err() == nil {
m.emit(Event{Kind: "error", Provider: op.Name, Method: "oauth", Message: "timeout waiting for callback"})
m.emit(Event{Kind: "error", Provider: eventProvider, Method: "oauth", Message: "timeout waiting for callback"})
}
return
}
if res.Err != nil {
m.emit(Event{Kind: "error", Provider: op.Name, Method: "oauth", Message: res.Err.Error()})
m.emit(Event{Kind: "error", Provider: eventProvider, Method: "oauth", Message: res.Err.Error()})
return
}
@ -205,14 +209,14 @@ func (m *Manager) awaitOAuth(ctx context.Context, op OAuthProvider, cs *Callback
defer exCancel()
tok, err := op.Exchange(exCtx, res.Code, res.State, pkce)
if err != nil {
m.emit(Event{Kind: "error", Provider: op.Name, Method: "oauth", Message: err.Error()})
m.emit(Event{Kind: "error", Provider: eventProvider, Method: "oauth", Message: err.Error()})
return
}
if err := m.store.SetOAuth(op.Name, *tok); err != nil {
m.emit(Event{Kind: "error", Provider: op.Name, Method: "oauth", Message: err.Error()})
if err := m.store.SetOAuth(storeProvider, *tok); err != nil {
m.emit(Event{Kind: "error", Provider: eventProvider, Method: "oauth", Message: err.Error()})
return
}
m.emit(Event{Kind: "success", Provider: op.Name, Method: "oauth"})
m.emit(Event{Kind: "success", Provider: eventProvider, Method: "oauth"})
}
// StartKimiDeviceOAuth starts Kimi Code's device-code subscription login.
@ -258,18 +262,20 @@ func (m *Manager) StartManualOAuth(provider string) (string, error) {
if provider == "kimi" {
return m.StartKimiDeviceOAuth()
}
storeProvider := provider
var op OAuthProvider
switch provider {
case "anthropic":
op = AnthropicManualOAuth
case "openai":
case "openai", "openai-codex":
op = OpenAIOAuth
storeProvider = "openai"
case "google":
return "", fmt.Errorf("google login is api-key only; use api key login for gemini")
case "deepseek":
return "", fmt.Errorf("deepseek login is api-key only; use api key login")
default:
return "", fmt.Errorf("provider must be anthropic, openai, kimi, deepseek, or google")
return "", fmt.Errorf("provider must be anthropic, openai, openai-codex, kimi, deepseek, or google")
}
pkce, err := NewPKCE()
@ -283,6 +289,8 @@ func (m *Manager) StartManualOAuth(provider string) (string, error) {
m.mu.Lock()
m.manualOp = &op
m.manualStoreProvider = storeProvider
m.manualEventProvider = provider
m.manualPKCE = pkce
m.manualState = state
m.mu.Unlock()
@ -297,6 +305,8 @@ func (m *Manager) StartManualOAuth(provider string) (string, error) {
func (m *Manager) CompleteManualOAuth(ctx context.Context, input string) error {
m.mu.Lock()
op := m.manualOp
storeProvider := m.manualStoreProvider
eventProvider := m.manualEventProvider
pkce := m.manualPKCE
state := m.manualState
m.mu.Unlock()
@ -314,19 +324,21 @@ func (m *Manager) CompleteManualOAuth(ctx context.Context, input string) error {
defer cancel()
tok, err := op.Exchange(exCtx, code, state, pkce)
if err != nil {
m.emit(Event{Kind: "error", Provider: op.Name, Method: "oauth", Message: err.Error()})
m.emit(Event{Kind: "error", Provider: eventProvider, Method: "oauth", Message: err.Error()})
return err
}
if err := m.store.SetOAuth(op.Name, *tok); err != nil {
m.emit(Event{Kind: "error", Provider: op.Name, Method: "oauth", Message: err.Error()})
if err := m.store.SetOAuth(storeProvider, *tok); err != nil {
m.emit(Event{Kind: "error", Provider: eventProvider, Method: "oauth", Message: err.Error()})
return err
}
m.mu.Lock()
m.manualOp = nil
m.manualStoreProvider = ""
m.manualEventProvider = ""
m.manualPKCE = PKCE{}
m.manualState = ""
m.mu.Unlock()
m.emit(Event{Kind: "success", Provider: op.Name, Method: "oauth"})
m.emit(Event{Kind: "success", Provider: eventProvider, Method: "oauth"})
return nil
}

View file

@ -23,8 +23,9 @@ type Credentials struct {
DeepSeek ProviderCreds `json:"deepseek,omitempty"`
}
// ProviderCreds holds credentials for a single provider. Only one of
// APIKey or OAuth is populated at a time.
// ProviderCreds holds credentials for a single provider. Most providers
// use either APIKey or OAuth; OpenAI may store both so the public API
// route and ChatGPT/Codex subscription route can coexist.
type ProviderCreds struct {
APIKey string `json:"api_key,omitempty"`
OAuth *OAuthToken `json:"oauth,omitempty"`
@ -138,7 +139,9 @@ func (s *Store) SetAPIKey(provider, key string) error {
return fmt.Errorf("unknown provider %q", provider)
}
p.APIKey = key
p.OAuth = nil
if provider != "openai" {
p.OAuth = nil
}
return s.saveLocked(c)
}
@ -154,7 +157,9 @@ func (s *Store) SetOAuth(provider string, tok OAuthToken) error {
if p == nil {
return fmt.Errorf("unknown provider %q", provider)
}
p.APIKey = ""
if provider != "openai" {
p.APIKey = ""
}
p.OAuth = &tok
return s.saveLocked(c)
}
@ -175,6 +180,38 @@ func (s *Store) Clear(provider string) error {
return s.saveLocked(c)
}
// ClearAPIKey removes only the API key for provider, preserving any OAuth token.
func (s *Store) ClearAPIKey(provider string) error {
s.mu.Lock()
defer s.mu.Unlock()
c, err := s.loadLocked()
if err != nil {
return err
}
p := c.get(provider)
if p == nil {
return fmt.Errorf("unknown provider %q", provider)
}
p.APIKey = ""
return s.saveLocked(c)
}
// ClearOAuth removes only the OAuth token for provider, preserving any API key.
func (s *Store) ClearOAuth(provider string) error {
s.mu.Lock()
defer s.mu.Unlock()
c, err := s.loadLocked()
if err != nil {
return err
}
p := c.get(provider)
if p == nil {
return fmt.Errorf("unknown provider %q", provider)
}
p.OAuth = nil
return s.saveLocked(c)
}
func (s *Store) saveLocked(c Credentials) error {
if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil {
return err

View file

@ -245,12 +245,8 @@ var Catalog = []Model{
},
// ---- Speculative: OpenAI ----
// Context windows on the OpenAI gpt-5.x family differ by route:
// the direct API advertises 400k, the ChatGPT Codex OAuth backend
// caps at 272k. zot serves both auth modes from one catalog row
// per id, so we pin to the smaller number to keep the context-usage
// meter honest under subscription auth. Users on the direct API
// simply see a conservative headroom estimate.
// Public OpenAI API route. The ChatGPT/Codex subscription route is
// represented separately below as provider "openai-codex".
{
Provider: "openai", ID: "gpt-5.1", DisplayName: "GPT-5.1",
ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
@ -279,12 +275,7 @@ var Catalog = []Model{
},
{
Provider: "openai", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini",
// ContextWindow: 400k on the OpenAI direct API, 272k on the
// ChatGPT Codex OAuth backend. We pin to the smaller Codex
// cap so the context-usage meter is honest under subscription
// auth; direct-API users simply see a conservative headroom
// estimate rather than an inflated one.
ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
PriceInput: 0.75, PriceOutput: 4.50, PriceCacheRead: 0.075,
Speculative: true,
},
@ -300,6 +291,52 @@ var Catalog = []Model{
PriceInput: 0.75, PriceOutput: 4.50, PriceCacheRead: 0.075,
Speculative: true,
},
// ---- OpenAI Codex / ChatGPT subscription backend ----
// Same model ids as the OpenAI family, but routed through the
// ChatGPT Codex OAuth backend rather than api.openai.com.
{
Provider: "openai-codex", ID: "gpt-5.2", DisplayName: "GPT-5.2 Codex",
ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
PriceInput: 1.75, PriceOutput: 14.00, PriceCacheRead: 0.175,
Speculative: true,
},
{
Provider: "openai-codex", ID: "gpt-5.3-codex", DisplayName: "GPT-5.3 Codex",
ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
PriceInput: 1.75, PriceOutput: 14.00, PriceCacheRead: 0.175,
Speculative: true,
},
{
Provider: "openai-codex", ID: "gpt-5.3-codex-spark", DisplayName: "GPT-5.3 Codex Spark",
ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
PriceInput: 1.75, PriceOutput: 14.00, PriceCacheRead: 0.175,
Speculative: true,
},
{
Provider: "openai-codex", ID: "gpt-5.4", DisplayName: "GPT-5.4 Codex",
ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
PriceInput: 2.50, PriceOutput: 15.00, PriceCacheRead: 0.25,
Speculative: true,
},
{
Provider: "openai-codex", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini Codex",
ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
PriceInput: 0.75, PriceOutput: 4.50, PriceCacheRead: 0.075,
Speculative: true,
},
{
Provider: "openai-codex", ID: "gpt-5.5", DisplayName: "GPT-5.5 Codex",
ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
PriceInput: 2.50, PriceOutput: 15.00, PriceCacheRead: 0.25,
Speculative: true,
},
{
Provider: "openai-codex", ID: "gpt-5.5-mini", DisplayName: "GPT-5.5 mini Codex",
ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
PriceInput: 0.75, PriceOutput: 4.50, PriceCacheRead: 0.075,
Speculative: true,
},
}
// DefaultModel is used when the user does not specify one.

View file

@ -57,7 +57,7 @@ func NewOpenAICodex(token, accountID, baseURL string) Client {
}
}
func (c *codexClient) Name() string { return "openai" }
func (c *codexClient) Name() string { return "openai-codex" }
// ---- Responses API wire types (subset needed for zot's surface) ----
@ -146,7 +146,10 @@ type codexRequest struct {
// ---- Request building ----
func (c *codexClient) buildRequest(req Request) (*codexRequest, error) {
m, err := FindModel("openai", req.Model)
m, err := FindModel("openai-codex", req.Model)
if err != nil {
m, err = FindModel("openai", req.Model)
}
if err != nil {
return nil, err
}
@ -324,8 +327,11 @@ func (c *codexClient) runStream(ctx context.Context, resp *http.Response, req Re
defer close(out)
defer resp.Body.Close()
model, _ := FindModel("openai", req.Model)
out <- EventStart{Model: req.Model, Provider: "openai"}
model, _ := FindModel("openai-codex", req.Model)
if model.ID == "" {
model, _ = FindModel("openai", req.Model)
}
out <- EventStart{Model: req.Model, Provider: "openai-codex"}
raw := make(chan sseEvent, 16)
go readSSE(resp.Body, raw)

View file

@ -70,11 +70,10 @@ func LoadUserModels(path string) []Model {
var out []Model
for providerName, prov := range file.Providers {
// Normalize provider name: strip suffixes like "-codex" so
// "openai-codex" maps to "openai".
// Normalize legacy transport aliases to their provider names.
normalized := providerName
switch providerName {
case "openai-codex", "openai-responses":
case "openai-responses":
normalized = "openai"
case "anthropic-messages":
normalized = "anthropic"