@bitkyc08/opencodex 2.7.42 → 2.7.43
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.
- package/README.md +8 -1
- package/bin/ocx.mjs +13 -14
- package/gui/dist/assets/index-Czw-jpTU.css +1 -0
- package/gui/dist/assets/index-cmds12BG.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +49 -6
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/inbound.ts +26 -9
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/claude.ts +3 -3
- package/src/cli/doctor.ts +23 -18
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +88 -19
- package/src/cli/interactive-confirm.ts +129 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +55 -16
- package/src/cli/status.ts +47 -13
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/parsing.ts +16 -1
- package/src/codex/catalog/provider-fetch.ts +48 -12
- package/src/codex/catalog/sync.ts +18 -3
- package/src/codex/catalog.ts +1 -1
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +15 -0
- package/src/codex/routing.ts +695 -106
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +566 -25
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +122 -4
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +164 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +283 -72
- package/src/oauth/key-providers.ts +17 -6
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +322 -10
- package/src/oauth/kiro.ts +423 -43
- package/src/oauth/login-cli.ts +32 -5
- package/src/oauth/store.ts +54 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +11 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +14 -4
- package/src/server/auth-cors.ts +110 -22
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/management/agent-settings-routes.ts +28 -4
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +5 -2
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +35 -7
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +2 -2
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +583 -152
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +49 -2
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +366 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +242 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/tray/windows.ts +2 -0
- package/src/types.ts +96 -1
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +103 -12
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/log.ts +4 -0
- package/gui/dist/assets/index-Bl_VBGoI.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
|
@@ -9,13 +9,16 @@ import { isCodexAccountUsable } from "./account-usability";
|
|
|
9
9
|
import { reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
|
|
10
10
|
import { MAIN_CODEX_ACCOUNT_ID, getMainAccountToken } from "./main-account";
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
codexQuotaScopeForModel,
|
|
13
|
+
getCodexQuotaHealthSnapshot,
|
|
13
14
|
releaseCodexQuotaProbeLease,
|
|
15
|
+
releaseCodexQuotaScopeProbeLease,
|
|
14
16
|
tryAcquireCodexQuotaProbeLease,
|
|
15
|
-
|
|
17
|
+
tryAcquireCodexQuotaScopeProbeLease,
|
|
18
|
+
pickAlternateCodexAccount,
|
|
16
19
|
resolveCodexAccountForThreadDetailed,
|
|
17
20
|
} from "./routing";
|
|
18
|
-
import type { CodexCooldownSource } from "./routing";
|
|
21
|
+
import type { CodexCooldownSource, CodexQuotaScope } from "./routing";
|
|
19
22
|
import { maskAccountId } from "../lib/privacy";
|
|
20
23
|
import { formatErrorResponse } from "../bridge";
|
|
21
24
|
import { getAccountQuota } from "./quota";
|
|
@@ -34,8 +37,12 @@ export type CodexAuthContext =
|
|
|
34
37
|
* Set when this request was admitted through an active quota cooldown as
|
|
35
38
|
* the account's single probe. Must be echoed into the upstream outcome so
|
|
36
39
|
* only this request can clear the cooldown (#433).
|
|
37
|
-
|
|
40
|
+
*/
|
|
38
41
|
probeLeaseId?: string;
|
|
42
|
+
/** Native model quota group selected for this request, when known. */
|
|
43
|
+
quotaScope?: CodexQuotaScope;
|
|
44
|
+
/** Scope that owns `probeLeaseId`, when it is a scoped recovery probe. */
|
|
45
|
+
probeQuotaScope?: CodexQuotaScope;
|
|
39
46
|
}
|
|
40
47
|
| {
|
|
41
48
|
// Main Codex account participating in rotation: token injected from ~/.codex/auth.json
|
|
@@ -46,6 +53,8 @@ export type CodexAuthContext =
|
|
|
46
53
|
chatgptAccountId: string;
|
|
47
54
|
/** See `pool.probeLeaseId`. */
|
|
48
55
|
probeLeaseId?: string;
|
|
56
|
+
quotaScope?: CodexQuotaScope;
|
|
57
|
+
probeQuotaScope?: CodexQuotaScope;
|
|
49
58
|
};
|
|
50
59
|
|
|
51
60
|
/** Probe lease carried by this context, when it holds one. */
|
|
@@ -53,13 +62,20 @@ export function codexProbeLeaseId(ctx: CodexAuthContext | undefined): string | u
|
|
|
53
62
|
return ctx?.kind === "pool" || ctx?.kind === "main-pool" ? ctx.probeLeaseId : undefined;
|
|
54
63
|
}
|
|
55
64
|
|
|
65
|
+
/** Scope of a lease carried by this context, when it probes a model-specific quota. */
|
|
66
|
+
export function codexProbeQuotaScope(ctx: CodexAuthContext | undefined): CodexQuotaScope | undefined {
|
|
67
|
+
return ctx?.kind === "pool" || ctx?.kind === "main-pool" ? ctx.probeQuotaScope : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
/**
|
|
57
71
|
* Hand back a probe lease for a request that will not reach upstream. Safe to
|
|
58
72
|
* call with a context that holds no lease.
|
|
59
73
|
*/
|
|
60
74
|
export function releaseCodexAuthContextProbeLease(ctx: CodexAuthContext | undefined): void {
|
|
61
75
|
const leaseId = codexProbeLeaseId(ctx);
|
|
62
|
-
if (ctx
|
|
76
|
+
if (!ctx || ctx.kind === "main" || !leaseId) return;
|
|
77
|
+
if (ctx.probeQuotaScope) releaseCodexQuotaScopeProbeLease(ctx.accountId!, ctx.probeQuotaScope, leaseId);
|
|
78
|
+
else releaseCodexQuotaProbeLease(ctx.accountId!, leaseId);
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
export type OcxRuntimeProviderConfig = OcxProviderConfig & {
|
|
@@ -99,13 +115,20 @@ export class CodexAccountCooldownError extends Error {
|
|
|
99
115
|
accountId: string;
|
|
100
116
|
cooldownUntil: number;
|
|
101
117
|
cooldownSource?: CodexCooldownSource;
|
|
118
|
+
quotaScope?: CodexQuotaScope;
|
|
102
119
|
|
|
103
|
-
constructor(
|
|
120
|
+
constructor(
|
|
121
|
+
accountId: string,
|
|
122
|
+
cooldownUntil: number,
|
|
123
|
+
cooldownSource?: CodexCooldownSource,
|
|
124
|
+
quotaScope?: CodexQuotaScope,
|
|
125
|
+
) {
|
|
104
126
|
super("Selected Codex account is cooling down");
|
|
105
127
|
this.name = "CodexAccountCooldownError";
|
|
106
128
|
this.accountId = accountId;
|
|
107
129
|
this.cooldownUntil = cooldownUntil;
|
|
108
130
|
this.cooldownSource = cooldownSource;
|
|
131
|
+
this.quotaScope = quotaScope;
|
|
109
132
|
}
|
|
110
133
|
}
|
|
111
134
|
|
|
@@ -127,7 +150,12 @@ export function cooldownAccountLabel(accountId: string): string {
|
|
|
127
150
|
*/
|
|
128
151
|
export function cooldownErrorMessage(err: CodexAccountCooldownError): string {
|
|
129
152
|
const until = new Date(err.cooldownUntil).toISOString();
|
|
130
|
-
|
|
153
|
+
const scope = err.quotaScope === "spark"
|
|
154
|
+
? "Spark quota"
|
|
155
|
+
: err.quotaScope === "shared"
|
|
156
|
+
? "shared native quota"
|
|
157
|
+
: null;
|
|
158
|
+
return `Selected Codex account (${cooldownAccountLabel(err.accountId)})${scope ? ` ${scope} is` : " is"} cooling down until ${until}`
|
|
131
159
|
+ ` (source: ${err.cooldownSource ?? "default"}).`
|
|
132
160
|
+ ` Run 'ocx account list openai' to find the id, then`
|
|
133
161
|
+ ` 'ocx account clear-cooldown openai <id>' to lift it, or switch accounts with 'ocx account use openai <id>'.`;
|
|
@@ -157,6 +185,8 @@ export function shouldMarkAccountNeedsReauthForCodexAuthFailure(cause: unknown):
|
|
|
157
185
|
|
|
158
186
|
export interface ResolveCodexAuthContextOptions {
|
|
159
187
|
excludeAccountId?: string;
|
|
188
|
+
/** Final native model selected for this request, used to select its quota group. */
|
|
189
|
+
modelId?: string;
|
|
160
190
|
}
|
|
161
191
|
|
|
162
192
|
export async function resolveCodexAuthContext(
|
|
@@ -171,16 +201,17 @@ export async function resolveCodexAuthContext(
|
|
|
171
201
|
}
|
|
172
202
|
reconcileMainCodexAccountRuntimeState();
|
|
173
203
|
const threadId = headers.get("x-codex-parent-thread-id");
|
|
204
|
+
const quotaScope = codexQuotaScopeForModel(options.modelId);
|
|
174
205
|
const resolution = options.excludeAccountId
|
|
175
206
|
? (() => {
|
|
176
|
-
const accountId =
|
|
207
|
+
const accountId = pickAlternateCodexAccount(config, options.excludeAccountId!, Date.now(), quotaScope);
|
|
177
208
|
return accountId
|
|
178
209
|
? { status: "selected" as const, accountId }
|
|
179
210
|
: { status: "none" as const };
|
|
180
211
|
})()
|
|
181
|
-
: resolveCodexAccountForThreadDetailed(threadId, config);
|
|
212
|
+
: resolveCodexAccountForThreadDetailed(threadId, config, Date.now(), quotaScope);
|
|
182
213
|
if (resolution.status === "expired") throw new CodexThreadAffinityExpiredError(resolution.accountId);
|
|
183
|
-
|
|
214
|
+
let accountId = resolution.status === "selected" ? resolution.accountId : null;
|
|
184
215
|
if (!accountId) throw new CodexPoolAuthenticationError();
|
|
185
216
|
// Lazy prime: if the selected account has no quota yet, the pool is likely
|
|
186
217
|
// unprimed (dashboard never opened, or startup prime was blocked). Kick a
|
|
@@ -194,15 +225,21 @@ export async function resolveCodexAuthContext(
|
|
|
194
225
|
}
|
|
195
226
|
// Snapshot (not just the deadline) so a refused request can report WHY it is cooled:
|
|
196
227
|
// a literal Retry-After reads very differently to a user than a reset-derived guess.
|
|
197
|
-
const cooldown =
|
|
228
|
+
const cooldown = getCodexQuotaHealthSnapshot(accountId, quotaScope);
|
|
198
229
|
const cooldownUntil = cooldown?.cooldownUntil;
|
|
199
230
|
// A cooled-down account never sends traffic, so upstream recovery can never be
|
|
200
231
|
// observed and the cooldown outlives the real limit. Admit one probe per
|
|
201
232
|
// interval; its outcome decides whether the cooldown ends (#433).
|
|
202
233
|
let probeLeaseId: string | undefined;
|
|
234
|
+
let probeQuotaScope: CodexQuotaScope | undefined;
|
|
203
235
|
if (cooldownUntil) {
|
|
204
|
-
|
|
205
|
-
|
|
236
|
+
probeQuotaScope = cooldown?.quotaScope;
|
|
237
|
+
probeLeaseId = probeQuotaScope
|
|
238
|
+
? tryAcquireCodexQuotaScopeProbeLease(accountId, probeQuotaScope) ?? undefined
|
|
239
|
+
: tryAcquireCodexQuotaProbeLease(accountId) ?? undefined;
|
|
240
|
+
if (!probeLeaseId) {
|
|
241
|
+
throw new CodexAccountCooldownError(accountId, cooldownUntil, cooldown?.cooldownSource, cooldown?.quotaScope);
|
|
242
|
+
}
|
|
206
243
|
}
|
|
207
244
|
|
|
208
245
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
@@ -210,7 +247,8 @@ export async function resolveCodexAuthContext(
|
|
|
210
247
|
const token = getMainAccountToken();
|
|
211
248
|
if (!token) {
|
|
212
249
|
// Nothing will reach upstream, so give the probe back instead of burning it.
|
|
213
|
-
if (probeLeaseId)
|
|
250
|
+
if (probeLeaseId && probeQuotaScope) releaseCodexQuotaScopeProbeLease(accountId, probeQuotaScope, probeLeaseId);
|
|
251
|
+
else if (probeLeaseId) releaseCodexQuotaProbeLease(accountId, probeLeaseId);
|
|
214
252
|
throw new CodexPoolAuthenticationError();
|
|
215
253
|
}
|
|
216
254
|
return {
|
|
@@ -218,7 +256,9 @@ export async function resolveCodexAuthContext(
|
|
|
218
256
|
accountId,
|
|
219
257
|
accessToken: token.accessToken,
|
|
220
258
|
chatgptAccountId: token.chatgptAccountId,
|
|
259
|
+
...(quotaScope ? { quotaScope } : {}),
|
|
221
260
|
...(probeLeaseId ? { probeLeaseId } : {}),
|
|
261
|
+
...(probeQuotaScope ? { probeQuotaScope } : {}),
|
|
222
262
|
};
|
|
223
263
|
}
|
|
224
264
|
|
|
@@ -230,10 +270,13 @@ export async function resolveCodexAuthContext(
|
|
|
230
270
|
generation: token.generation,
|
|
231
271
|
accessToken: token.accessToken,
|
|
232
272
|
chatgptAccountId: token.chatgptAccountId,
|
|
273
|
+
...(quotaScope ? { quotaScope } : {}),
|
|
233
274
|
...(probeLeaseId ? { probeLeaseId } : {}),
|
|
275
|
+
...(probeQuotaScope ? { probeQuotaScope } : {}),
|
|
234
276
|
};
|
|
235
277
|
} catch (cause) {
|
|
236
|
-
if (probeLeaseId)
|
|
278
|
+
if (probeLeaseId && probeQuotaScope) releaseCodexQuotaScopeProbeLease(accountId, probeQuotaScope, probeLeaseId);
|
|
279
|
+
else if (probeLeaseId) releaseCodexQuotaProbeLease(accountId, probeLeaseId);
|
|
237
280
|
if (shouldMarkAccountNeedsReauthForCodexAuthFailure(cause)) {
|
|
238
281
|
markAccountNeedsReauth(accountId);
|
|
239
282
|
}
|
|
@@ -245,9 +288,9 @@ export function assertCodexAuthContextNotCooled(ctx: CodexAuthContext | undefine
|
|
|
245
288
|
if (ctx?.kind !== "pool" && ctx?.kind !== "main-pool") return;
|
|
246
289
|
// A context holding the probe lease was deliberately admitted through the cooldown.
|
|
247
290
|
if (ctx.probeLeaseId) return;
|
|
248
|
-
const cooldown =
|
|
291
|
+
const cooldown = getCodexQuotaHealthSnapshot(ctx.accountId, ctx.quotaScope);
|
|
249
292
|
if (cooldown?.cooldownUntil) {
|
|
250
|
-
throw new CodexAccountCooldownError(ctx.accountId, cooldown.cooldownUntil, cooldown.cooldownSource);
|
|
293
|
+
throw new CodexAccountCooldownError(ctx.accountId, cooldown.cooldownUntil, cooldown.cooldownSource, cooldown.quotaScope);
|
|
251
294
|
}
|
|
252
295
|
}
|
|
253
296
|
|
|
@@ -141,9 +141,24 @@ export function isMediaGenerationModelId(id: string): boolean {
|
|
|
141
141
|
return MEDIA_GEN_ID_RE.test(id);
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Gemini image-capable chat models produce inline images within text responses
|
|
146
|
+
* via the Responses API. Explicit allowlist only — a broad `/gemini/ && /image/`
|
|
147
|
+
* heuristic resurrects standalone media-gen IDs (e.g. gemini-3-pro-image).
|
|
148
|
+
*/
|
|
149
|
+
const GEMINI_IMAGE_CHAT_MODEL_IDS = new Set([
|
|
150
|
+
"gemini-3.1-flash-image",
|
|
151
|
+
"gemini-2.0-flash-preview-image-generation",
|
|
152
|
+
"gemini-3-pro-image-preview",
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
function isGeminiImageChatModel(id: string): boolean {
|
|
156
|
+
return GEMINI_IMAGE_CHAT_MODEL_IDS.has(id);
|
|
157
|
+
}
|
|
158
|
+
|
|
144
159
|
export function shouldExposeRoutedModel(model: CatalogModel): boolean {
|
|
145
160
|
if (isRoutedModelCompatibilityExcluded(`${model.provider}/${model.id}`)) return false;
|
|
146
|
-
if (model.
|
|
161
|
+
if (isGeminiImageChatModel(model.id)) return true;
|
|
147
162
|
return !isMediaGenerationModelId(model.id);
|
|
148
163
|
}
|
|
149
164
|
|
|
@@ -39,7 +39,11 @@ import {
|
|
|
39
39
|
targetKey,
|
|
40
40
|
} from "../../combos";
|
|
41
41
|
import type { NormalizedComboConfig } from "../../combos/types";
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
ProviderOutboundPolicyError,
|
|
44
|
+
providerOutboundGet,
|
|
45
|
+
providerRedirectError,
|
|
46
|
+
} from "../../lib/provider-outbound";
|
|
43
47
|
import { redactSecretString } from "../../lib/redact";
|
|
44
48
|
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
45
49
|
|
|
@@ -71,6 +75,32 @@ export function isProviderModelsApiItems(value: unknown): value is ProviderModel
|
|
|
71
75
|
);
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Normalize OpenAI-compatible /models payloads for catalog discovery.
|
|
80
|
+
* Supports `{ data: [...] }` and top-level arrays (Together AI `#617`).
|
|
81
|
+
* Google's `{ models: [...] }` is handled by the connectivity probe only — catalog
|
|
82
|
+
* discovery must not treat a stray `models` key on openai-chat responses as valid.
|
|
83
|
+
*/
|
|
84
|
+
export function providerModelsListFromResponse(json: unknown): unknown {
|
|
85
|
+
if (Array.isArray(json)) return json;
|
|
86
|
+
if (json !== null && typeof json === "object" && !Array.isArray(json)) {
|
|
87
|
+
const data = (json as { data?: unknown }).data;
|
|
88
|
+
if (Array.isArray(data)) return data;
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Connectivity-probe shape: also accepts Google `{ models: [...] }`. */
|
|
94
|
+
export function providerModelsListFromProbeResponse(json: unknown): unknown {
|
|
95
|
+
if (Array.isArray(json)) return json;
|
|
96
|
+
if (json !== null && typeof json === "object" && !Array.isArray(json)) {
|
|
97
|
+
const obj = json as { data?: unknown; models?: unknown };
|
|
98
|
+
if (Array.isArray(obj.data)) return obj.data;
|
|
99
|
+
if (Array.isArray(obj.models)) return obj.models;
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
74
104
|
export function configuredContextWindow(prov: OcxProviderConfig, id: string): number | undefined {
|
|
75
105
|
const configured = modelRecordValue(prov.modelContextWindows, id) ?? prov.contextWindow;
|
|
76
106
|
return typeof configured === "number" && configured > 0 ? configured : undefined;
|
|
@@ -322,21 +352,20 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
322
352
|
};
|
|
323
353
|
};
|
|
324
354
|
try {
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
355
|
+
const res = await providerOutboundGet(name, prov, url, {
|
|
356
|
+
headers,
|
|
357
|
+
signal: AbortSignal.timeout(8000),
|
|
328
358
|
});
|
|
329
|
-
|
|
330
|
-
|
|
359
|
+
const redirectError = await providerRedirectError(res, url);
|
|
360
|
+
if (redirectError) {
|
|
361
|
+
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "http", httpStatus: res.status });
|
|
331
362
|
if (shouldLog) {
|
|
332
363
|
console.warn(
|
|
333
|
-
`[opencodex] Provider model discovery for "${name}"
|
|
364
|
+
`[opencodex] Provider model discovery for "${name}" ${redirectError} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
334
365
|
);
|
|
335
366
|
}
|
|
336
367
|
return models;
|
|
337
368
|
}
|
|
338
|
-
|
|
339
|
-
const res = await fetch(url, { headers, signal: AbortSignal.timeout(8000) });
|
|
340
369
|
if (!res.ok) {
|
|
341
370
|
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "http", httpStatus: res.status });
|
|
342
371
|
if (shouldLog) {
|
|
@@ -366,9 +395,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
366
395
|
}
|
|
367
396
|
return models;
|
|
368
397
|
}
|
|
369
|
-
const data =
|
|
370
|
-
? (json as { data?: unknown }).data
|
|
371
|
-
: undefined;
|
|
398
|
+
const data = providerModelsListFromResponse(json);
|
|
372
399
|
if (!isProviderModelsApiItems(data)) {
|
|
373
400
|
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "invalid_response" });
|
|
374
401
|
if (shouldLog) {
|
|
@@ -421,6 +448,15 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
421
448
|
setCached(name, live);
|
|
422
449
|
return live;
|
|
423
450
|
} catch (error) {
|
|
451
|
+
if (error instanceof ProviderOutboundPolicyError) {
|
|
452
|
+
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "blocked" });
|
|
453
|
+
if (shouldLog) {
|
|
454
|
+
console.warn(
|
|
455
|
+
`[opencodex] Provider model discovery for "${name}" was blocked by destination policy: ${error.message} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return models;
|
|
459
|
+
}
|
|
424
460
|
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "network" });
|
|
425
461
|
if (shouldLog) {
|
|
426
462
|
console.warn(
|
|
@@ -329,9 +329,11 @@ export function mergeCatalogEntriesForSync(
|
|
|
329
329
|
multiAgentMode: MultiAgentMode = "default",
|
|
330
330
|
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
331
331
|
hasPhysicalComboProvider = false,
|
|
332
|
+
includeNativeOpenAi = true,
|
|
332
333
|
): RawEntry[] {
|
|
333
334
|
const rank = new Map(featured.map((slug, i) => [slug, i] as const));
|
|
334
|
-
const native =
|
|
335
|
+
const native = includeNativeOpenAi
|
|
336
|
+
? catalogModels
|
|
335
337
|
.filter(m => typeof m.slug === "string"
|
|
336
338
|
&& !(m.slug as string).includes("/")
|
|
337
339
|
&& m.owned_by !== COMBO_NAMESPACE
|
|
@@ -367,11 +369,14 @@ export function mergeCatalogEntriesForSync(
|
|
|
367
369
|
// for subagent max spawns; wire-clamped to the model's real top rung).
|
|
368
370
|
if (!isGpt56NativeSlug(slug)) ensureUltraReasoningLevel(preserved);
|
|
369
371
|
return preserved;
|
|
370
|
-
})
|
|
372
|
+
})
|
|
373
|
+
: [];
|
|
371
374
|
|
|
372
375
|
// Backfill any native OpenAI slug that the on-disk catalog is missing (e.g. gpt-5.5), so a
|
|
373
376
|
// routed provider exposing the same id can never delete the native OpenAI/Codex base row.
|
|
377
|
+
// Skip when no enabled canonical openai provider exists (#636) — bare gpt-* would 404.
|
|
374
378
|
const nativeSlugs = new Set(native.flatMap(m => typeof m.slug === "string" ? [m.slug] : []));
|
|
379
|
+
if (includeNativeOpenAi) {
|
|
375
380
|
for (const slug of nativeOpenAiSlugs()) {
|
|
376
381
|
if (nativeSlugs.has(slug)) continue;
|
|
377
382
|
nativeSlugs.add(slug);
|
|
@@ -382,6 +387,7 @@ export function mergeCatalogEntriesForSync(
|
|
|
382
387
|
: 9;
|
|
383
388
|
native.push(deriveEntry(template ? JSON.parse(JSON.stringify(template)) : null, slug, "OpenAI native model (Codex OAuth passthrough).", priority));
|
|
384
389
|
}
|
|
390
|
+
}
|
|
385
391
|
|
|
386
392
|
const freshSlugs = new Set(
|
|
387
393
|
routedEntries.flatMap(entry => typeof entry.slug === "string" ? [entry.slug] : []),
|
|
@@ -497,7 +503,16 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{
|
|
|
497
503
|
// native AND routed so the advertised flag matches the implemented endpoint (phase 120.4) and a
|
|
498
504
|
// native template can never leak supports_websockets while the flag is off.
|
|
499
505
|
const wsEnabled = websocketsEnabled(config);
|
|
500
|
-
|
|
506
|
+
const enabledProviders = Object.entries(config.providers ?? {})
|
|
507
|
+
.filter(([, prov]) => prov.disabled !== true);
|
|
508
|
+
const hasCanonicalOpenai = enabledProviders.some(([name, prov]) =>
|
|
509
|
+
name === "openai" && isCanonicalOpenAiForwardProvider(prov),
|
|
510
|
+
);
|
|
511
|
+
// #636: when the user only configured non-OpenAI providers (e.g. kimi), do not advertise
|
|
512
|
+
// bare gpt-* rows that hard-404 via NoEnabledOpenAiProviderError. Keep natives when no
|
|
513
|
+
// providers are configured yet (fresh install / catalog bootstrap tests).
|
|
514
|
+
const includeNativeOpenAi = enabledProviders.length === 0 || hasCanonicalOpenai;
|
|
515
|
+
catalog.models = mergeCatalogEntriesForSync(catalog.models ?? [], goEntries, baseline, featured, wsEnabled, goIds, template, disabledNativeSlugs(config), gatheredProviderNames, multiAgentMode, exactComboSlugs, hasPhysicalComboProvider, includeNativeOpenAi);
|
|
501
516
|
clampCatalogModelsToCodexSupport(catalog.models);
|
|
502
517
|
|
|
503
518
|
atomicWriteFile(catalogPath, JSON.stringify(catalog, null, 2) + "\n");
|
package/src/codex/catalog.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// AUTO-SPLIT facade: original catalog.ts body moved into ./catalog/* modules.
|
|
2
2
|
// Public surface preserved exactly; importers keep using "src/codex/catalog".
|
|
3
|
-
export { isMediaGenerationModelId, readCodexCatalogPath, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing";
|
|
3
|
+
export { isMediaGenerationModelId, shouldExposeRoutedModel, readCodexCatalogPath, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing";
|
|
4
4
|
export type { CatalogModel, MultiAgentMode } from "./catalog/parsing";
|
|
5
5
|
export { NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs } from "./catalog/metadata";
|
|
6
6
|
export { isSpawnableCodexCandidate, codexExecInvocation, loadBundledCodexCatalog, materializeBundledCodexCatalog, loadCatalogTemplate } from "./catalog/bundled";
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync, unlinkSync, writeSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { zstdDecompressSync } from "node:zlib";
|
|
4
5
|
import { Database } from "bun:sqlite";
|
|
5
6
|
import { CODEX_HOME } from "./paths";
|
|
6
7
|
import { atomicWriteFile, getConfigDir } from "../config";
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Cap for decompressing a lone `.jsonl.zst` rollout during quarantine restore.
|
|
11
|
+
* Bounds peak memory while reconstructing thread rows; never write the decoded
|
|
12
|
+
* JSONL to disk.
|
|
13
|
+
*/
|
|
14
|
+
export const MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
|
|
15
|
+
|
|
8
16
|
const STATE_DB_PATH = join(CODEX_HOME, "state_5.sqlite");
|
|
9
17
|
function historyBackupPathFor(stateDbPath: string): string {
|
|
10
18
|
const normalized = process.platform === "win32" ? resolve(stateDbPath).toLowerCase() : resolve(stateDbPath);
|
|
@@ -252,7 +260,7 @@ function parseSessionMetaLine(line: string): ParsedSessionMeta | null {
|
|
|
252
260
|
* (codex-rs `apply_session_meta_from_item`). We base our patch on the most recent metadata so we
|
|
253
261
|
* never resurrect a stale provider that a later app-written `session_meta` already changed.
|
|
254
262
|
*/
|
|
255
|
-
function readLatestSessionMeta(path: string): ParsedSessionMeta | null {
|
|
263
|
+
export function readLatestSessionMeta(path: string): ParsedSessionMeta | null {
|
|
256
264
|
const raw = readFileSync(path, "utf8");
|
|
257
265
|
const lines = raw.split("\n");
|
|
258
266
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -265,6 +273,142 @@ function readLatestSessionMeta(path: string): ParsedSessionMeta | null {
|
|
|
265
273
|
return null;
|
|
266
274
|
}
|
|
267
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Fields needed to re-insert a production-shaped `threads` row from a rollout JSONL when a
|
|
278
|
+
* Phase-2 quarantine predates full `satellite-backup.json` thread snapshots.
|
|
279
|
+
*
|
|
280
|
+
* Uses the same last-writer-wins `session_meta` fold as {@link readLatestSessionMeta}, plus the
|
|
281
|
+
* first user-message preview (codex-rs `list.rs` / `EventMsg::UserMessage` path).
|
|
282
|
+
*/
|
|
283
|
+
export interface RolloutThreadFields {
|
|
284
|
+
id: string;
|
|
285
|
+
modelProvider: string;
|
|
286
|
+
source: string;
|
|
287
|
+
firstUserMessage: string;
|
|
288
|
+
hasUserEvent: number;
|
|
289
|
+
cwd?: string;
|
|
290
|
+
historyMode?: string;
|
|
291
|
+
cliVersion?: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function textFromContentParts(content: unknown): string | null {
|
|
295
|
+
if (typeof content === "string" && content.trim()) return content.trim();
|
|
296
|
+
if (!Array.isArray(content)) return null;
|
|
297
|
+
const parts: string[] = [];
|
|
298
|
+
for (const part of content) {
|
|
299
|
+
if (!part || typeof part !== "object") continue;
|
|
300
|
+
const p = part as Record<string, unknown>;
|
|
301
|
+
if (typeof p.text === "string" && p.text.trim()) parts.push(p.text.trim());
|
|
302
|
+
else if (typeof p.input_text === "string" && p.input_text.trim()) parts.push(p.input_text.trim());
|
|
303
|
+
}
|
|
304
|
+
const joined = parts.join("\n").trim();
|
|
305
|
+
return joined || null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Extract the first user-message preview from a rollout line, or null. */
|
|
309
|
+
function extractUserMessagePreview(line: string): string | null {
|
|
310
|
+
let parsed: unknown;
|
|
311
|
+
try {
|
|
312
|
+
parsed = JSON.parse(line);
|
|
313
|
+
} catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
317
|
+
const record = parsed as { type?: unknown; payload?: unknown };
|
|
318
|
+
const payload = record.payload;
|
|
319
|
+
if (!payload || typeof payload !== "object") return null;
|
|
320
|
+
const p = payload as Record<string, unknown>;
|
|
321
|
+
|
|
322
|
+
if (record.type === "event_msg") {
|
|
323
|
+
// codex-rs EventMsg::UserMessage — payload.type is "user_message" (or omitted in fixtures).
|
|
324
|
+
if (p.type === "user_message" || typeof p.message === "string") {
|
|
325
|
+
if (typeof p.message === "string" && p.message.trim()) return p.message.trim();
|
|
326
|
+
const fromContent = textFromContentParts(p.content);
|
|
327
|
+
if (fromContent) return fromContent;
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (record.type === "response_item") {
|
|
333
|
+
if (p.type === "message" && p.role === "user") {
|
|
334
|
+
return textFromContentParts(p.content);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Reconstruct thread identity + listing fields from a staged/restored rollout JSONL.
|
|
342
|
+
* Returns null when the file is missing or has no parseable `session_meta`.
|
|
343
|
+
*
|
|
344
|
+
* Accepts plain `.jsonl` or a lone `.jsonl.zst` (legacy Phase-2 quarantine). Compressed
|
|
345
|
+
* rollouts are decompressed in memory with {@link MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES};
|
|
346
|
+
* no decompressed copy is written to disk.
|
|
347
|
+
*/
|
|
348
|
+
export function readThreadFieldsFromRollout(path: string): RolloutThreadFields | null {
|
|
349
|
+
if (!path || !existsSync(path)) return null;
|
|
350
|
+
let raw: string;
|
|
351
|
+
try {
|
|
352
|
+
raw = path.endsWith(".zst")
|
|
353
|
+
? decompressRolloutZstUtf8(path)
|
|
354
|
+
: readFileSync(path, "utf8");
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
return parseThreadFieldsFromRolloutText(raw);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function decompressRolloutZstUtf8(
|
|
362
|
+
path: string,
|
|
363
|
+
maxBytes: number = MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES,
|
|
364
|
+
): string {
|
|
365
|
+
const compressed = readFileSync(path);
|
|
366
|
+
const decoded = zstdDecompressSync(compressed as Uint8Array<ArrayBuffer>, {
|
|
367
|
+
maxOutputLength: maxBytes,
|
|
368
|
+
});
|
|
369
|
+
if (decoded.byteLength > maxBytes) {
|
|
370
|
+
throw new Error("rollout_zst_too_large");
|
|
371
|
+
}
|
|
372
|
+
return new TextDecoder().decode(decoded);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function parseThreadFieldsFromRolloutText(raw: string): RolloutThreadFields | null {
|
|
376
|
+
const lines = raw.split("\n");
|
|
377
|
+
let latest: ParsedSessionMeta | null = null;
|
|
378
|
+
let firstUserMessage = "";
|
|
379
|
+
for (const line of lines) {
|
|
380
|
+
if (!line) continue;
|
|
381
|
+
if (line.includes("\"session_meta\"")) {
|
|
382
|
+
const meta = parseSessionMetaLine(line);
|
|
383
|
+
if (meta) latest = meta;
|
|
384
|
+
}
|
|
385
|
+
if (!firstUserMessage) {
|
|
386
|
+
const preview = extractUserMessagePreview(line);
|
|
387
|
+
if (preview) firstUserMessage = preview;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (!latest) return null;
|
|
391
|
+
const payload = latest.record.payload;
|
|
392
|
+
const id = typeof payload.id === "string" ? payload.id : "";
|
|
393
|
+
if (!id) return null;
|
|
394
|
+
const modelProvider = typeof payload.model_provider === "string" && payload.model_provider
|
|
395
|
+
? payload.model_provider
|
|
396
|
+
: "openai";
|
|
397
|
+
const source = typeof payload.source === "string" && payload.source
|
|
398
|
+
? payload.source
|
|
399
|
+
: "cli";
|
|
400
|
+
return {
|
|
401
|
+
id,
|
|
402
|
+
modelProvider,
|
|
403
|
+
source,
|
|
404
|
+
firstUserMessage,
|
|
405
|
+
hasUserEvent: firstUserMessage.trim() ? 1 : 0,
|
|
406
|
+
...(typeof payload.cwd === "string" ? { cwd: payload.cwd } : {}),
|
|
407
|
+
...(typeof payload.history_mode === "string" ? { historyMode: payload.history_mode } : {}),
|
|
408
|
+
...(typeof payload.cli_version === "string" ? { cliVersion: payload.cli_version } : {}),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
268
412
|
/**
|
|
269
413
|
* Make a thread's rollout reflect a provider/source change by APPENDING a new `session_meta` line,
|
|
270
414
|
* rather than rewriting line 1. The appended line clones the latest metadata payload (so no field
|