@bitkyc08/opencodex 2.7.23 → 2.7.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.ko.md +37 -7
  2. package/README.md +52 -11
  3. package/README.zh-CN.md +36 -7
  4. package/bin/ocx.mjs +5 -3
  5. package/gui/dist/assets/index-BzhyTAco.js +40 -0
  6. package/gui/dist/assets/index-Dq3eZ1cU.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/opencode.svg +1 -1
  9. package/package.json +5 -2
  10. package/src/adapters/anthropic-image-normalize.ts +70 -29
  11. package/src/adapters/cursor/transport-retry.ts +20 -1
  12. package/src/adapters/run-turn-queue.ts +40 -0
  13. package/src/codex/auth-api.ts +10 -1
  14. package/src/codex/auth-context.ts +33 -7
  15. package/src/codex/catalog.ts +357 -23
  16. package/src/codex/routing.ts +10 -4
  17. package/src/combos/failover.ts +102 -0
  18. package/src/combos/index.ts +37 -0
  19. package/src/combos/request.ts +31 -0
  20. package/src/combos/resolve.ts +171 -0
  21. package/src/combos/types.ts +203 -0
  22. package/src/config.ts +280 -11
  23. package/src/lib/errors.ts +86 -24
  24. package/src/lib/upstream-retry.ts +8 -4
  25. package/src/oauth/index.ts +7 -1
  26. package/src/oauth/key-providers.ts +2 -32
  27. package/src/oauth/login-cli.ts +4 -3
  28. package/src/oauth/token-guardian.ts +38 -3
  29. package/src/providers/derive.ts +27 -2
  30. package/src/providers/kiro-models.ts +8 -3
  31. package/src/providers/label.ts +3 -1
  32. package/src/providers/openai-sidecar.ts +94 -0
  33. package/src/providers/openai-tier-startup.ts +27 -0
  34. package/src/providers/openai-tiers.ts +283 -0
  35. package/src/providers/openai-virtual-models.ts +82 -0
  36. package/src/providers/quota.ts +344 -24
  37. package/src/providers/registry.ts +112 -20
  38. package/src/reasoning-effort.ts +12 -11
  39. package/src/router.ts +80 -36
  40. package/src/server/auth-cors.ts +85 -9
  41. package/src/server/images.ts +31 -75
  42. package/src/server/index.ts +45 -86
  43. package/src/server/management-api.ts +273 -21
  44. package/src/server/request-log.ts +221 -20
  45. package/src/server/responses.ts +594 -75
  46. package/src/server/search.ts +22 -37
  47. package/src/types.ts +49 -1
  48. package/src/update/index.ts +50 -6
  49. package/src/update/job.ts +21 -4
  50. package/src/usage/log.ts +124 -1
  51. package/src/usage/summary.ts +147 -56
  52. package/src/vision/index.ts +20 -19
  53. package/src/web-search/index.ts +15 -17
  54. package/gui/dist/assets/index-Bk_GgFrh.css +0 -1
  55. package/gui/dist/assets/index-DQjt6Hly.js +0 -40
@@ -24,6 +24,9 @@ import {
24
24
  TokenRefreshError,
25
25
  } from "../codex/account-store";
26
26
  import { codexWarmupFailureReason, warmCodexAccount } from "../codex/warmup";
27
+ import { getMainAccountToken, MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
28
+ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
29
+ import { providerCodexAccountMode } from "../providers/registry";
27
30
 
28
31
  export interface TokenGuardianHandle {
29
32
  stop(): void;
@@ -147,9 +150,41 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
147
150
  }
148
151
  }
149
152
 
150
- // B) multi-account Codex pool (gated on the chatgpt provider's policy)
151
- if (resolveRefreshPolicy("chatgpt", config) === "proactive") {
152
- for (const id of listCodexAccountIds()) {
153
+ // B) canonical OpenAI Codex login. Direct warms main only; pool additionally maintains every
154
+ // added account. The direct branch returns before the managed account store is enumerated.
155
+ const openai = config.providers[OPENAI_CODEX_PROVIDER_ID];
156
+ if (
157
+ openai
158
+ && openai.disabled !== true
159
+ && isCanonicalOpenAiForwardProvider(openai)
160
+ && resolveRefreshPolicy(OPENAI_CODEX_PROVIDER_ID, config) === "proactive"
161
+ ) {
162
+ const mode = providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) ?? "pool";
163
+ if (opts.codexWarmupEnabled) {
164
+ const mainToken = getMainAccountToken();
165
+ const key = `codex:${MAIN_CODEX_ACCOUNT_ID}`;
166
+ if (mainToken && !inBackoff(key, nowMs)) {
167
+ tasks.push(async () => {
168
+ try {
169
+ await warmCodexAccount({
170
+ accessToken: mainToken.accessToken,
171
+ chatgptAccountId: mainToken.chatgptAccountId,
172
+ model: opts.codexWarmupModel,
173
+ });
174
+ backoff.delete(key);
175
+ result.warmed.push(key);
176
+ } catch (err) {
177
+ recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, false);
178
+ result.failed.push(key);
179
+ }
180
+ });
181
+ } else if (mainToken) {
182
+ result.skippedBackoff.push(key);
183
+ }
184
+ }
185
+
186
+ const addedAccountIds = mode === "pool" ? listCodexAccountIds() : [];
187
+ for (const id of addedAccountIds) {
153
188
  const record = readCodexAccountRecord(id);
154
189
  if (!record || record.deletedAt != null) continue;
155
190
  const cred = record.credential;
@@ -1,4 +1,4 @@
1
- import type { OcxProviderConfig } from "../types";
1
+ import type { CodexAccountMode, OcxProviderConfig } from "../types";
2
2
  import { PROVIDER_REGISTRY, type ProviderRegistryEntry } from "./registry";
3
3
 
4
4
  export interface DerivedKeyLoginProvider {
@@ -12,8 +12,10 @@ export interface DerivedKeyLoginProvider {
12
12
  contextWindow?: number;
13
13
  modelContextWindows?: Record<string, number>;
14
14
  modelInputModalities?: Record<string, string[]>;
15
+ modelMaxInputTokens?: Record<string, number>;
15
16
  reasoningEfforts?: string[];
16
17
  modelReasoningEfforts?: Record<string, string[]>;
18
+ modelDefaultReasoningEfforts?: Record<string, string>;
17
19
  reasoningEffortMap?: Record<string, string>;
18
20
  modelReasoningEffortMap?: Record<string, Record<string, string>>;
19
21
  noVisionModels?: string[];
@@ -26,6 +28,9 @@ export interface DerivedKeyLoginProvider {
26
28
  thinkingToggleModels?: string[];
27
29
  thinkingBudgetModels?: string[];
28
30
  escapeBuiltinToolNames?: boolean;
31
+ googleMode?: "ai-studio" | "vertex" | "cloud-code-assist";
32
+ project?: string;
33
+ location?: string;
29
34
  }
30
35
 
31
36
  export interface DerivedInitProvider {
@@ -34,6 +39,7 @@ export interface DerivedInitProvider {
34
39
  adapter: string;
35
40
  baseUrl: string;
36
41
  kind: "forward" | "oauth" | "key" | "local";
42
+ codexAccountMode?: CodexAccountMode;
37
43
  dashboardUrl?: string;
38
44
  defaultModel?: string;
39
45
  }
@@ -45,10 +51,15 @@ export interface DerivedProviderPreset {
45
51
  baseUrl: string;
46
52
  defaultModel?: string;
47
53
  auth: "oauth" | "forward" | "key" | "local";
54
+ codexAccountMode?: CodexAccountMode;
48
55
  oauthProvider?: string;
49
56
  dashboardUrl?: string;
50
57
  note?: string;
51
58
  keyOptional?: boolean;
59
+ /** Free pricing (may still require a key). */
60
+ freeTier?: boolean;
61
+ /** Immutable canonical provider config seed for the reserved canonical `openai` forward preset. */
62
+ provider?: OcxProviderConfig;
52
63
  }
53
64
 
54
65
  export function listRegistryEntries(): readonly ProviderRegistryEntry[] {
@@ -68,7 +79,9 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
68
79
  adapter: entry.adapter,
69
80
  baseUrl: entry.baseUrl,
70
81
  authMode: entry.authKind === "local" ? undefined : entry.authKind,
82
+ ...(entry.codexAccountMode ? { codexAccountMode: entry.codexAccountMode } : {}),
71
83
  ...(entry.keyOptional !== undefined ? { keyOptional: entry.keyOptional } : {}),
84
+ ...(entry.freeTier !== undefined ? { freeTier: entry.freeTier } : {}),
72
85
  ...(entry.modelSuffixBracketStrip !== undefined ? { modelSuffixBracketStrip: entry.modelSuffixBracketStrip } : {}),
73
86
  ...(entry.staticHeaders ? { headers: { ...entry.staticHeaders } } : {}),
74
87
  ...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
@@ -77,8 +90,10 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
77
90
  ...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
78
91
  ...(entry.modelContextWindows ? { modelContextWindows: { ...entry.modelContextWindows } } : {}),
79
92
  ...(entry.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(entry.modelInputModalities) } : {}),
93
+ ...(entry.modelMaxInputTokens ? { modelMaxInputTokens: { ...entry.modelMaxInputTokens } } : {}),
80
94
  ...(entry.reasoningEfforts ? { reasoningEfforts: [...entry.reasoningEfforts] } : {}),
81
95
  ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(entry.modelReasoningEfforts) } : {}),
96
+ ...(entry.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...entry.modelDefaultReasoningEfforts } } : {}),
82
97
  ...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}),
83
98
  ...(entry.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(entry.modelReasoningEffortMap) } : {}),
84
99
  ...(entry.noVisionModels ? { noVisionModels: [...entry.noVisionModels] } : {}),
@@ -114,8 +129,10 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
114
129
  ...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
115
130
  ...(entry.modelContextWindows ? { modelContextWindows: { ...entry.modelContextWindows } } : {}),
116
131
  ...(entry.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(entry.modelInputModalities) } : {}),
132
+ ...(entry.modelMaxInputTokens ? { modelMaxInputTokens: { ...entry.modelMaxInputTokens } } : {}),
117
133
  ...(entry.reasoningEfforts ? { reasoningEfforts: [...entry.reasoningEfforts] } : {}),
118
134
  ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(entry.modelReasoningEfforts) } : {}),
135
+ ...(entry.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...entry.modelDefaultReasoningEfforts } } : {}),
119
136
  ...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}),
120
137
  ...(entry.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(entry.modelReasoningEffortMap) } : {}),
121
138
  ...(entry.noVisionModels ? { noVisionModels: [...entry.noVisionModels] } : {}),
@@ -143,6 +160,7 @@ export function deriveInitProviders(): DerivedInitProvider[] {
143
160
  adapter: entry.adapter,
144
161
  baseUrl: entry.baseUrl,
145
162
  kind: entry.authKind,
163
+ ...(entry.codexAccountMode ? { codexAccountMode: entry.codexAccountMode } : {}),
146
164
  ...(entry.dashboardUrl ? { dashboardUrl: entry.dashboardUrl } : {}),
147
165
  ...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
148
166
  }));
@@ -173,6 +191,8 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
173
191
  if (!entry) return;
174
192
  const seed = providerConfigSeed(entry);
175
193
  if (!prov.defaultModel && seed.defaultModel) prov.defaultModel = seed.defaultModel;
194
+ // Fill mode only when absent: an explicit persisted `direct` must never be overwritten.
195
+ if (prov.codexAccountMode === undefined && seed.codexAccountMode !== undefined) prov.codexAccountMode = seed.codexAccountMode;
176
196
  if (!prov.models && seed.models) prov.models = [...seed.models];
177
197
  if (prov.liveModels === undefined && seed.liveModels !== undefined) prov.liveModels = seed.liveModels;
178
198
  if (prov.contextWindow === undefined && seed.contextWindow !== undefined) prov.contextWindow = seed.contextWindow;
@@ -180,6 +200,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
180
200
  if (!prov.modelInputModalities && seed.modelInputModalities) prov.modelInputModalities = cloneRecordOfArrays(seed.modelInputModalities);
181
201
  if (!prov.reasoningEfforts && seed.reasoningEfforts) prov.reasoningEfforts = [...seed.reasoningEfforts];
182
202
  if (!prov.modelReasoningEfforts && seed.modelReasoningEfforts) prov.modelReasoningEfforts = cloneRecordOfArrays(seed.modelReasoningEfforts);
203
+ if (!prov.modelDefaultReasoningEfforts && seed.modelDefaultReasoningEfforts) prov.modelDefaultReasoningEfforts = { ...seed.modelDefaultReasoningEfforts };
183
204
  if (!prov.reasoningEffortMap && seed.reasoningEffortMap) prov.reasoningEffortMap = { ...seed.reasoningEffortMap };
184
205
  if (!prov.modelReasoningEffortMap && seed.modelReasoningEffortMap) prov.modelReasoningEffortMap = cloneNestedRecord(seed.modelReasoningEffortMap);
185
206
  if (!prov.noVisionModels && seed.noVisionModels) prov.noVisionModels = [...seed.noVisionModels];
@@ -194,6 +215,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
194
215
  if (!prov.thinkingBudgetModels && seed.thinkingBudgetModels) prov.thinkingBudgetModels = [...seed.thinkingBudgetModels];
195
216
  if (prov.escapeBuiltinToolNames === undefined && seed.escapeBuiltinToolNames !== undefined) prov.escapeBuiltinToolNames = seed.escapeBuiltinToolNames;
196
217
  if (prov.keyOptional === undefined && seed.keyOptional !== undefined) prov.keyOptional = seed.keyOptional;
218
+ if (prov.freeTier === undefined && seed.freeTier !== undefined) prov.freeTier = seed.freeTier;
197
219
  if (prov.modelSuffixBracketStrip === undefined && seed.modelSuffixBracketStrip !== undefined) prov.modelSuffixBracketStrip = seed.modelSuffixBracketStrip;
198
220
  if (!prov.headers && seed.headers) prov.headers = { ...seed.headers };
199
221
  }
@@ -226,11 +248,14 @@ function entryToPreset(entry: ProviderRegistryEntry): DerivedProviderPreset {
226
248
  adapter: entry.adapter,
227
249
  baseUrl: entry.baseUrl,
228
250
  auth: entry.authKind === "forward" ? "forward" : entry.authKind === "oauth" ? "oauth" : entry.authKind === "local" ? "local" : "key",
251
+ ...(entry.codexAccountMode ? { codexAccountMode: entry.codexAccountMode } : {}),
252
+ ...(entry.codexAccountMode ? { provider: providerConfigSeed(entry) } : {}),
229
253
  ...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
230
254
  ...(entry.authKind === "oauth" ? { oauthProvider: entry.oauthId ?? entry.id } : {}),
231
255
  ...(entry.dashboardUrl ? { dashboardUrl: entry.dashboardUrl } : {}),
232
256
  ...(entry.note ? { note: entry.note } : {}),
233
257
  ...(entry.keyOptional ? { keyOptional: true } : {}),
258
+ ...(entry.freeTier ? { freeTier: true } : {}),
234
259
  };
235
260
  }
236
261
 
@@ -250,7 +275,7 @@ function customPreset(): DerivedProviderPreset {
250
275
  }
251
276
 
252
277
  function formatInitLabel(entry: ProviderRegistryEntry): string {
253
- if (entry.authKind === "forward") return "OpenAI — ChatGPT login (no key)";
278
+ if (entry.authKind === "forward") return "OpenAI — ChatGPT login (no key; account pool default, Direct selectable)";
254
279
  if (entry.authKind === "oauth") {
255
280
  if (entry.id === "xai") return "xAI (Grok) — account login";
256
281
  if (entry.id === "anthropic") return "Anthropic (Claude) — account login";
@@ -1,5 +1,9 @@
1
1
  export const KIRO_MODELS = [
2
2
  "kiro-auto",
3
+ // OpenAI GPT-5.6 (Kiro experimental, us-east-1) — added to official catalog 2026-07-13
4
+ "gpt-5.6-sol",
5
+ "gpt-5.6-terra",
6
+ "gpt-5.6-luna",
3
7
  "claude-sonnet-5",
4
8
  "claude-opus-4.8",
5
9
  "claude-opus-4.7",
@@ -17,11 +21,12 @@ export const KIRO_MODELS = [
17
21
  ];
18
22
 
19
23
  // Per-model context windows as documented on Kiro's official model catalog
20
- // (https://kiro.dev/docs/models/ — "Quick comparison", page updated 2026-06-19).
21
- // claude-sonnet-5 is pre-seeded from Anthropic/OpenRouter 1M metadata until Kiro's
22
- // public catalog catches up.
24
+ // (https://kiro.dev/docs/models/ — "Quick comparison", page updated 2026-07-14).
23
25
  // "Auto" is a router with no fixed window on Kiro's table, so it is intentionally omitted.
24
26
  export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
27
+ "gpt-5.6-sol": 272_000,
28
+ "gpt-5.6-terra": 272_000,
29
+ "gpt-5.6-luna": 272_000,
25
30
  "claude-sonnet-5": 1_000_000,
26
31
  "claude-opus-4.8": 1_000_000,
27
32
  "claude-opus-4.7": 1_000_000,
@@ -1,10 +1,12 @@
1
1
  import { CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
2
2
 
3
3
  function canonicalUsageProviderLabel(provider: string): string {
4
- return provider === "chatgpt" ? "openai" : provider;
4
+ return provider === "chatgpt" || provider === "openai-multi" ? "openai" : provider;
5
5
  }
6
6
 
7
7
  export function baseProviderLabel(provider: string): string {
8
+ const canonical = canonicalUsageProviderLabel(provider);
9
+ if (canonical !== provider) return canonical;
8
10
  const cut = provider.lastIndexOf("-");
9
11
  if (cut <= 0) return canonicalUsageProviderLabel(provider);
10
12
  const suffix = provider.slice(cut + 1);
@@ -0,0 +1,94 @@
1
+ import { resolveEnvValue } from "../config";
2
+ import {
3
+ headersForCodexAuthContext,
4
+ hasCallerCodexBearer,
5
+ isCodexAuthContextUsable,
6
+ resolveCodexAuthContext,
7
+ type CodexAuthContext,
8
+ } from "../codex/auth-context";
9
+ import { recordCodexUpstreamOutcome, type CodexUpstreamOutcome } from "../codex/routing";
10
+ import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../server/auth-cors";
11
+ import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
12
+ import {
13
+ isCanonicalOpenAiForwardProvider,
14
+ OPENAI_API_PROVIDER_ID,
15
+ OPENAI_CODEX_PROVIDER_ID,
16
+ } from "./openai-tiers";
17
+ import { providerCodexAccountMode } from "./registry";
18
+
19
+ export interface OpenAiForwardSidecarCandidate {
20
+ providerName: typeof OPENAI_CODEX_PROVIDER_ID;
21
+ provider: OcxProviderConfig;
22
+ accountMode: CodexAccountMode;
23
+ }
24
+
25
+ export interface ResolvedOpenAiForwardSidecar extends OpenAiForwardSidecarCandidate {
26
+ authContext: CodexAuthContext;
27
+ headers: Headers;
28
+ recordOutcome?: (outcome: CodexUpstreamOutcome) => void;
29
+ }
30
+
31
+ export interface OpenAiImagesProviderSelection {
32
+ forwardCandidates: OpenAiForwardSidecarCandidate[];
33
+ keyed?: {
34
+ providerName: typeof OPENAI_API_PROVIDER_ID;
35
+ provider: OcxProviderConfig;
36
+ apiKey: string;
37
+ };
38
+ }
39
+
40
+ export function listOpenAiForwardSidecarCandidates(config: OcxConfig): OpenAiForwardSidecarCandidate[] {
41
+ const provider = config.providers[OPENAI_CODEX_PROVIDER_ID];
42
+ if (!provider || provider.disabled === true || !isCanonicalOpenAiForwardProvider(provider)) return [];
43
+ return [{
44
+ providerName: OPENAI_CODEX_PROVIDER_ID,
45
+ provider,
46
+ accountMode: providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, provider) ?? "pool",
47
+ }];
48
+ }
49
+
50
+ export async function resolveFirstUsableOpenAiSidecar(
51
+ candidates: readonly OpenAiForwardSidecarCandidate[],
52
+ incomingHeaders: Headers,
53
+ config: OcxConfig,
54
+ ): Promise<ResolvedOpenAiForwardSidecar | undefined> {
55
+ let callerBearerMayBeForwarded = true;
56
+ try {
57
+ validateForwardAdmissionCredential(incomingHeaders, config);
58
+ } catch (error) {
59
+ if (!(error instanceof ForwardAdmissionCredentialError)) throw error;
60
+ callerBearerMayBeForwarded = false;
61
+ }
62
+ for (const candidate of candidates) {
63
+ if (candidate.accountMode === "direct" && (!callerBearerMayBeForwarded || !hasCallerCodexBearer(incomingHeaders))) continue;
64
+ const authContext = await resolveCodexAuthContext(incomingHeaders, config, candidate.accountMode);
65
+ if (!isCodexAuthContextUsable(authContext, config)) continue;
66
+ return {
67
+ ...candidate,
68
+ authContext,
69
+ headers: headersForCodexAuthContext(incomingHeaders, authContext),
70
+ ...(authContext.kind === "pool" || authContext.kind === "main-pool"
71
+ ? { recordOutcome: (outcome: CodexUpstreamOutcome) => recordCodexUpstreamOutcome(config, authContext.accountId, outcome) }
72
+ : {}),
73
+ };
74
+ }
75
+ return undefined;
76
+ }
77
+
78
+ export function selectOpenAiImagesProvider(config: OcxConfig): OpenAiImagesProviderSelection {
79
+ const selection: OpenAiImagesProviderSelection = {
80
+ forwardCandidates: listOpenAiForwardSidecarCandidates(config),
81
+ };
82
+ const provider = config.providers[OPENAI_API_PROVIDER_ID];
83
+ if (
84
+ provider
85
+ && provider.disabled !== true
86
+ && provider.adapter === "openai-responses"
87
+ && provider.authMode !== "forward"
88
+ && provider.baseUrl.replace(/\/+$/, "") === "https://api.openai.com/v1"
89
+ ) {
90
+ const apiKey = resolveEnvValue(provider.apiKey)?.trim();
91
+ if (apiKey) selection.keyed = { providerName: OPENAI_API_PROVIDER_ID, provider, apiKey };
92
+ }
93
+ return selection;
94
+ }
@@ -0,0 +1,27 @@
1
+ import { backupConfigBeforeOpenAiTierMigration, saveConfig } from "../config";
2
+ import type { OcxConfig } from "../types";
3
+ import { projectOpenAiTierMigration } from "./openai-tiers";
4
+
5
+ export interface OpenAiTierStartupDeps {
6
+ project: typeof projectOpenAiTierMigration;
7
+ backup: () => void;
8
+ save: (config: OcxConfig) => void;
9
+ }
10
+
11
+ const DEFAULT_DEPS: OpenAiTierStartupDeps = {
12
+ project: projectOpenAiTierMigration,
13
+ backup: backupConfigBeforeOpenAiTierMigration,
14
+ save: saveConfig,
15
+ };
16
+
17
+ export function runOpenAiTierStartupMigration(
18
+ config: OcxConfig,
19
+ deps: OpenAiTierStartupDeps = DEFAULT_DEPS,
20
+ ): OcxConfig {
21
+ const projection = deps.project(config);
22
+ if (!projection.changed) return projection.config;
23
+ deps.backup();
24
+ deps.save(projection.config);
25
+ for (const warning of projection.warnings) console.warn(`[openai-provider-migration] ${warning}`);
26
+ return projection.config;
27
+ }
@@ -0,0 +1,283 @@
1
+ import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
2
+ import { OPENAI_PROVIDER_TIER_VERSION } from "../types";
3
+
4
+ export const OPENAI_CODEX_PROVIDER_ID = "openai";
5
+ export const LEGACY_OPENAI_MULTI_PROVIDER_ID = "openai-multi";
6
+ export const OPENAI_API_PROVIDER_ID = "openai-apikey";
7
+ export const LEGACY_CHATGPT_PROVIDER_ID = "chatgpt";
8
+
9
+ const CODEX_FORWARD_BASE_URL = "https://chatgpt.com/backend-api/codex";
10
+ const LEGACY_OPENAI_MULTI_PREFIX = `${LEGACY_OPENAI_MULTI_PROVIDER_ID}/`;
11
+
12
+ function canonicalCodexForwardProvider(mode: CodexAccountMode): OcxProviderConfig {
13
+ return {
14
+ adapter: "openai-responses",
15
+ baseUrl: CODEX_FORWARD_BASE_URL,
16
+ authMode: "forward",
17
+ codexAccountMode: mode,
18
+ };
19
+ }
20
+
21
+ function normalizedBaseUrl(value: string): string | undefined {
22
+ try {
23
+ const url = new URL(value.trim());
24
+ if (url.username || url.password || url.search || url.hash) return undefined;
25
+ const path = url.pathname.replace(/\/+$/, "");
26
+ return `${url.origin}${path}`;
27
+ } catch {
28
+ return undefined;
29
+ }
30
+ }
31
+
32
+ export function isCanonicalOpenAiForwardProvider(provider: OcxProviderConfig): boolean {
33
+ return provider.adapter === "openai-responses"
34
+ && provider.authMode === "forward"
35
+ && normalizedBaseUrl(provider.baseUrl) === CODEX_FORWARD_BASE_URL;
36
+ }
37
+
38
+ export interface OpenAiTierMigrationProjection {
39
+ config: OcxConfig;
40
+ changed: boolean;
41
+ resolvedMode: CodexAccountMode;
42
+ warnings: string[];
43
+ }
44
+
45
+ export class OpenAiTierMigrationCollisionError extends Error {
46
+ readonly providerName = LEGACY_OPENAI_MULTI_PROVIDER_ID;
47
+
48
+ constructor() {
49
+ super(`Reserved provider id "${LEGACY_OPENAI_MULTI_PROVIDER_ID}" is already configured with a noncanonical shape`);
50
+ this.name = "OpenAiTierMigrationCollisionError";
51
+ }
52
+ }
53
+
54
+ function managedLegacyMultiOverlay(provider: OcxProviderConfig): Pick<OcxProviderConfig, "disabled" | "selectedModels"> | null {
55
+ const allowed = new Set(["adapter", "authMode", "baseUrl", "disabled", "selectedModels"]);
56
+ if (!Object.keys(provider).every(key => allowed.has(key))) return null;
57
+ if (!isCanonicalOpenAiForwardProvider(provider)) return null;
58
+ if (provider.disabled !== undefined && typeof provider.disabled !== "boolean") return null;
59
+ if (provider.selectedModels !== undefined && (
60
+ !Array.isArray(provider.selectedModels)
61
+ || provider.selectedModels.some(model => typeof model !== "string")
62
+ )) return null;
63
+ return {
64
+ ...(provider.disabled !== undefined ? { disabled: provider.disabled } : {}),
65
+ ...(provider.selectedModels !== undefined ? { selectedModels: [...provider.selectedModels] } : {}),
66
+ };
67
+ }
68
+
69
+ function rewriteLegacyOpenAiSelectedId(value: string): string {
70
+ return value.startsWith(LEGACY_OPENAI_MULTI_PREFIX)
71
+ ? value.slice(LEGACY_OPENAI_MULTI_PREFIX.length)
72
+ : value;
73
+ }
74
+
75
+ function rewriteLegacyOpenAiModelList(values: string[] | undefined): string[] | undefined {
76
+ if (!values) return undefined;
77
+ return [...new Set(values.map(rewriteLegacyOpenAiSelectedId))];
78
+ }
79
+
80
+ function mergeLegacyOpenAiProviderRows(
81
+ openai: OcxProviderConfig | undefined,
82
+ legacyMulti: OcxProviderConfig | undefined,
83
+ mode: CodexAccountMode,
84
+ ): OcxProviderConfig {
85
+ const selectedModels = rewriteLegacyOpenAiModelList([
86
+ ...(openai?.selectedModels ?? []),
87
+ ...(legacyMulti?.selectedModels ?? []),
88
+ ]);
89
+ const formerRows = [openai, legacyMulti].filter((row): row is OcxProviderConfig => row !== undefined);
90
+ const disabled = formerRows.length > 0 && formerRows.every(row => row.disabled === true);
91
+ return {
92
+ ...canonicalCodexForwardProvider(mode),
93
+ ...(disabled ? { disabled: true } : {}),
94
+ ...(selectedModels && selectedModels.length > 0 ? { selectedModels } : {}),
95
+ };
96
+ }
97
+
98
+ function hasKnownLegacyOpenAiReference(config: OcxConfig): boolean {
99
+ const matches = (value: unknown): boolean => typeof value === "string" && value.startsWith(LEGACY_OPENAI_MULTI_PREFIX);
100
+ const matchesList = (value: unknown): boolean => Array.isArray(value) && value.some(matches);
101
+ const claude = config.claudeCode;
102
+ return config.defaultProvider === LEGACY_OPENAI_MULTI_PROVIDER_ID
103
+ || matchesList(config.disabledModels)
104
+ || matchesList(config.subagentModels)
105
+ || matches(config.injectionModel)
106
+ || matches(config.shadowCallIntercept?.model)
107
+ || matches(config.webSearchSidecar?.model)
108
+ || matches(config.visionSidecar?.model)
109
+ || matches(claude?.webSearchSidecar?.model)
110
+ || matches(claude?.visionSidecar?.model)
111
+ || matches(claude?.model)
112
+ || matches(claude?.smallFastModel)
113
+ || Object.values(claude?.tierModels ?? {}).some(matches)
114
+ || Object.values(claude?.modelMap ?? {}).some(matches)
115
+ || matchesList(config.providers[OPENAI_CODEX_PROVIDER_ID]?.selectedModels)
116
+ || matchesList(config.providers[LEGACY_OPENAI_MULTI_PROVIDER_ID]?.selectedModels)
117
+ || config.providerContextCaps?.[LEGACY_OPENAI_MULTI_PROVIDER_ID] !== undefined;
118
+ }
119
+
120
+ function rewriteLegacyOpenAiReferences(config: OcxConfig, warnings: string[]): void {
121
+ config.disabledModels = rewriteLegacyOpenAiModelList(config.disabledModels);
122
+ config.subagentModels = rewriteLegacyOpenAiModelList(config.subagentModels);
123
+ if (config.injectionModel) config.injectionModel = rewriteLegacyOpenAiSelectedId(config.injectionModel);
124
+ if (config.shadowCallIntercept?.model) {
125
+ config.shadowCallIntercept.model = rewriteLegacyOpenAiSelectedId(config.shadowCallIntercept.model);
126
+ }
127
+ if (config.webSearchSidecar?.model) config.webSearchSidecar.model = rewriteLegacyOpenAiSelectedId(config.webSearchSidecar.model);
128
+ if (config.visionSidecar?.model) config.visionSidecar.model = rewriteLegacyOpenAiSelectedId(config.visionSidecar.model);
129
+
130
+ const claude = config.claudeCode;
131
+ if (claude?.webSearchSidecar?.model) claude.webSearchSidecar.model = rewriteLegacyOpenAiSelectedId(claude.webSearchSidecar.model);
132
+ if (claude?.visionSidecar?.model) claude.visionSidecar.model = rewriteLegacyOpenAiSelectedId(claude.visionSidecar.model);
133
+ if (claude?.model) claude.model = rewriteLegacyOpenAiSelectedId(claude.model);
134
+ if (claude?.smallFastModel) claude.smallFastModel = rewriteLegacyOpenAiSelectedId(claude.smallFastModel);
135
+ if (claude?.tierModels) {
136
+ for (const tier of ["opus", "sonnet", "haiku", "fable"] as const) {
137
+ const value = claude.tierModels[tier];
138
+ if (value) claude.tierModels[tier] = rewriteLegacyOpenAiSelectedId(value);
139
+ }
140
+ }
141
+ if (claude?.modelMap) {
142
+ for (const key of Object.keys(claude.modelMap)) {
143
+ claude.modelMap[key] = rewriteLegacyOpenAiSelectedId(claude.modelMap[key]!);
144
+ }
145
+ }
146
+
147
+ const caps = config.providerContextCaps;
148
+ const legacyCap = caps?.[LEGACY_OPENAI_MULTI_PROVIDER_ID];
149
+ if (caps && legacyCap !== undefined) {
150
+ const currentCap = caps[OPENAI_CODEX_PROVIDER_ID];
151
+ if (currentCap !== undefined) {
152
+ caps[OPENAI_CODEX_PROVIDER_ID] = Math.min(currentCap, legacyCap);
153
+ warnings.push("providerContextCaps.openai + providerContextCaps.openai-multi: kept lower positive cap");
154
+ } else {
155
+ caps[OPENAI_CODEX_PROVIDER_ID] = legacyCap;
156
+ }
157
+ delete caps[LEGACY_OPENAI_MULTI_PROVIDER_ID];
158
+ }
159
+ }
160
+
161
+ function isKnownLegacyValuePath(path: readonly string[]): boolean {
162
+ const joined = path.join(".");
163
+ if (joined === "defaultProvider") return true;
164
+ if (/^(disabledModels|subagentModels)\.\d+$/.test(joined)) return true;
165
+ if (/^providers\.(openai|openai-multi)\.selectedModels\.\d+$/.test(joined)) return true;
166
+ return new Set([
167
+ "injectionModel",
168
+ "shadowCallIntercept.model",
169
+ "webSearchSidecar.model",
170
+ "visionSidecar.model",
171
+ "claudeCode.webSearchSidecar.model",
172
+ "claudeCode.visionSidecar.model",
173
+ "claudeCode.model",
174
+ "claudeCode.smallFastModel",
175
+ "claudeCode.tierModels.opus",
176
+ "claudeCode.tierModels.sonnet",
177
+ "claudeCode.tierModels.haiku",
178
+ "claudeCode.tierModels.fable",
179
+ ]).has(joined) || /^claudeCode\.modelMap\..+$/.test(joined);
180
+ }
181
+
182
+ function unknownLegacyOpenAiWarnings(config: OcxConfig): string[] {
183
+ const warnings = new Set<string>();
184
+ const visit = (value: unknown, path: string[]): void => {
185
+ if (typeof value === "string") {
186
+ if (value.includes(LEGACY_OPENAI_MULTI_PROVIDER_ID) && !isKnownLegacyValuePath(path)) {
187
+ warnings.add(`${path.join(".")}: legacy OpenAI provider id left unchanged`);
188
+ }
189
+ return;
190
+ }
191
+ if (!value || typeof value !== "object") return;
192
+ if (Array.isArray(value)) {
193
+ value.forEach((item, index) => visit(item, [...path, String(index)]));
194
+ return;
195
+ }
196
+ for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
197
+ visit(item, [...path, key]);
198
+ }
199
+ };
200
+ visit(config, []);
201
+ return [...warnings];
202
+ }
203
+
204
+ function resolvedOpenAiMode(
205
+ config: OcxConfig,
206
+ openai: OcxProviderConfig | undefined,
207
+ legacyMulti: OcxProviderConfig | undefined,
208
+ knownLegacyReference: boolean,
209
+ ): CodexAccountMode {
210
+ if (openai?.codexAccountMode === "pool" || openai?.codexAccountMode === "direct") {
211
+ return openai.codexAccountMode;
212
+ }
213
+ if (
214
+ (legacyMulti && legacyMulti.disabled !== true)
215
+ || config.defaultProvider === LEGACY_OPENAI_MULTI_PROVIDER_ID
216
+ || knownLegacyReference
217
+ ) return "pool";
218
+ if (config.openaiProviderTierVersion === 1 && openai) return "direct";
219
+ if (config.openaiProviderTierVersion === 1 && legacyMulti) return "pool";
220
+ return "pool";
221
+ }
222
+
223
+ export function projectOpenAiTierMigration(config: OcxConfig): OpenAiTierMigrationProjection {
224
+ const projected = structuredClone(config);
225
+ const legacyMulti = projected.providers[LEGACY_OPENAI_MULTI_PROVIDER_ID];
226
+ if (legacyMulti && !managedLegacyMultiOverlay(legacyMulti)) {
227
+ throw new OpenAiTierMigrationCollisionError();
228
+ }
229
+
230
+ const openai = projected.providers[OPENAI_CODEX_PROVIDER_ID];
231
+ const knownLegacyReference = hasKnownLegacyOpenAiReference(projected);
232
+ const resolvedMode = resolvedOpenAiMode(projected, openai, legacyMulti, knownLegacyReference);
233
+ if (
234
+ projected.openaiProviderTierVersion === OPENAI_PROVIDER_TIER_VERSION
235
+ && !legacyMulti
236
+ && !Object.hasOwn(projected.providers, LEGACY_CHATGPT_PROVIDER_ID)
237
+ && projected.defaultProvider !== LEGACY_OPENAI_MULTI_PROVIDER_ID
238
+ && !knownLegacyReference
239
+ ) {
240
+ return { config: projected, changed: false, resolvedMode, warnings: [] };
241
+ }
242
+
243
+ const warnings = unknownLegacyOpenAiWarnings(projected);
244
+ const referencesCodexForward = !!openai
245
+ || !!legacyMulti
246
+ || Object.hasOwn(projected.providers, LEGACY_CHATGPT_PROVIDER_ID)
247
+ || projected.defaultProvider === OPENAI_CODEX_PROVIDER_ID
248
+ || projected.defaultProvider === LEGACY_OPENAI_MULTI_PROVIDER_ID
249
+ || projected.defaultProvider === LEGACY_CHATGPT_PROVIDER_ID
250
+ || knownLegacyReference;
251
+ const mergedOpenAi = referencesCodexForward
252
+ ? mergeLegacyOpenAiProviderRows(openai, legacyMulti, resolvedMode)
253
+ : undefined;
254
+
255
+ const nextProviders: Array<[string, OcxProviderConfig]> = [];
256
+ let inserted = false;
257
+ for (const [name, provider] of Object.entries(projected.providers)) {
258
+ if (name === OPENAI_CODEX_PROVIDER_ID) {
259
+ if (mergedOpenAi && !inserted) nextProviders.push([OPENAI_CODEX_PROVIDER_ID, mergedOpenAi]);
260
+ inserted = true;
261
+ continue;
262
+ }
263
+ if (name === LEGACY_OPENAI_MULTI_PROVIDER_ID || name === LEGACY_CHATGPT_PROVIDER_ID) {
264
+ if (mergedOpenAi && !inserted && !openai) {
265
+ nextProviders.push([OPENAI_CODEX_PROVIDER_ID, mergedOpenAi]);
266
+ inserted = true;
267
+ }
268
+ continue;
269
+ }
270
+ nextProviders.push([name, provider]);
271
+ }
272
+ if (mergedOpenAi && !inserted) nextProviders.push([OPENAI_CODEX_PROVIDER_ID, mergedOpenAi]);
273
+ projected.providers = Object.fromEntries(nextProviders);
274
+
275
+ if (
276
+ projected.defaultProvider === LEGACY_OPENAI_MULTI_PROVIDER_ID
277
+ || projected.defaultProvider === LEGACY_CHATGPT_PROVIDER_ID
278
+ ) projected.defaultProvider = OPENAI_CODEX_PROVIDER_ID;
279
+
280
+ rewriteLegacyOpenAiReferences(projected, warnings);
281
+ projected.openaiProviderTierVersion = OPENAI_PROVIDER_TIER_VERSION;
282
+ return { config: projected, changed: true, resolvedMode, warnings: [...new Set(warnings)] };
283
+ }