mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-27 05:46:34 +02:00
Expose OpenAI Codex as separate provider
This commit is contained in:
parent
f9d14252dc
commit
14ffdb65b0
11 changed files with 200 additions and 66 deletions
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue