@bitkyc08/opencodex 2.49.0 → 2.50.0
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/AGENTS_INSTALL.md +9 -1
- package/README.md +3 -0
- package/gui/dist/assets/index-C39tnjXO.js +115 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/claude/inbound.ts +17 -5
- package/src/cli/account-api.ts +18 -3
- package/src/cli/account-auth.ts +8 -1
- package/src/cli/account-extended.ts +2 -1
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +15 -1
- package/src/cli/index.ts +5 -1
- package/src/cli/models-runtime.ts +8 -3
- package/src/cli/observe.ts +13 -3
- package/src/clients/config-export/zcode.ts +24 -0
- package/src/codex/account-runtime-state.ts +6 -1
- package/src/codex/account-store.ts +72 -9
- package/src/codex/account-usability.ts +3 -2
- package/src/codex/auth-api.ts +107 -23
- package/src/codex/auth-context.ts +21 -0
- package/src/codex/catalog/parsing.ts +23 -0
- package/src/codex/catalog/provider-fetch.ts +71 -2
- package/src/codex/catalog/sync.ts +14 -0
- package/src/codex/inject.ts +3 -2
- package/src/codex/quota-auto-refresh.ts +6 -1
- package/src/codex/quota.ts +54 -8
- package/src/combos/index.ts +2 -0
- package/src/combos/resolve.ts +52 -0
- package/src/config.ts +58 -0
- package/src/generated/compatibility-version.json +72 -60
- package/src/lib/errors.ts +8 -0
- package/src/lib/privacy.ts +25 -0
- package/src/oauth/health.ts +47 -12
- package/src/oauth/index.ts +46 -8
- package/src/oauth/token-guardian.ts +32 -6
- package/src/providers/google-ai-studio-model-discovery.ts +74 -0
- package/src/providers/opencode-zen-rate-limit.ts +75 -0
- package/src/providers/quota.ts +15 -0
- package/src/providers/registry.ts +1 -1
- package/src/server/auth-cors.ts +6 -0
- package/src/server/chat-completions.ts +4 -4
- package/src/server/chat-native.ts +10 -1
- package/src/server/claude-messages.ts +5 -5
- package/src/server/images.ts +2 -2
- package/src/server/index.ts +25 -2
- package/src/server/management/logs-usage-routes.ts +4 -1
- package/src/server/management/model-rows.ts +16 -1
- package/src/server/management/oauth-account-routes.ts +6 -2
- package/src/server/management/provider-routes.ts +9 -2
- package/src/server/management/request-history-routes.ts +4 -2
- package/src/server/management/route-registry.ts +5 -4
- package/src/server/management/shared.ts +66 -3
- package/src/server/management-api.ts +1 -1
- package/src/server/request-decompress.ts +91 -3
- package/src/server/request-log.ts +10 -0
- package/src/server/responses/codex-ws-wire.ts +1 -1
- package/src/server/responses/compact.ts +8 -2
- package/src/server/responses/context-overflow.ts +11 -0
- package/src/server/responses/core.ts +144 -38
- package/src/server/responses/policy-fallback.ts +6 -2
- package/src/server/search.ts +2 -2
- package/src/service.ts +92 -7
- package/src/types/accounts.ts +18 -0
- package/src/types/config.ts +36 -0
- package/src/types/provider.ts +56 -0
- package/src/types.ts +4 -0
- package/src/web-search/ollama-executor.ts +127 -0
- package/src/web-search/passthrough-bridge.ts +761 -0
- package/gui/dist/assets/index-BtyONQrZ.js +0 -115
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-C39tnjXO.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-BoBRSehJ.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
package/src/claude/inbound.ts
CHANGED
|
@@ -150,10 +150,17 @@ function blockedSkillCallIds(messages: readonly unknown[], blocked: readonly str
|
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* Claude Code (observed 2026-07-11, real CLI smoke) sends `role:"system"` entries in
|
|
153
|
-
* `messages` despite the published API having no system role.
|
|
154
|
-
*
|
|
155
|
-
* `
|
|
156
|
-
*
|
|
153
|
+
* `messages` despite the published API having no system role. They are emitted as
|
|
154
|
+
* chronological `role:"developer"` input items, which keeps the timeline intact and
|
|
155
|
+
* leaves `instructions` owned solely by the top-level Anthropic `system` field.
|
|
156
|
+
*
|
|
157
|
+
* The original mapping folded them into `instructions` because the native ChatGPT
|
|
158
|
+
* backend rejects `role:"system"` items in `input` ("System messages are not allowed",
|
|
159
|
+
* verified live). That constraint is real and still respected — but it only rules out
|
|
160
|
+
* `system`, not `developer`, which every Responses route accepts. Folding meant each
|
|
161
|
+
* mid-conversation reminder mutated the prompt head, invalidating the upstream KV
|
|
162
|
+
* prefix and rotating the Desktop `prompt_cache_key` fallback below on every turn
|
|
163
|
+
* (#4148).
|
|
157
164
|
*/
|
|
158
165
|
function systemMessageText(content: unknown): string {
|
|
159
166
|
if (typeof content === "string") return content;
|
|
@@ -333,7 +340,12 @@ function translateAnthropicRequest(raw: unknown, cc: OcxClaudeCodeConfig | undef
|
|
|
333
340
|
else if (msg.role === "assistant") assistantMessageToItems(msg.content, input, budget);
|
|
334
341
|
else if (msg.role === "system") {
|
|
335
342
|
const text = systemMessageText(msg.content);
|
|
336
|
-
|
|
343
|
+
// Keep it where the client put it. `developer` is first-class in the Responses
|
|
344
|
+
// schema and survives parseRequest as a chronological message, where `system`
|
|
345
|
+
// would be re-hoisted back onto the system prompt and defeat the point.
|
|
346
|
+
if (text.length > 0) {
|
|
347
|
+
input.push({ type: "message", role: "developer", content: [{ type: "input_text", text }] });
|
|
348
|
+
}
|
|
337
349
|
}
|
|
338
350
|
else throw new AnthropicRequestError(`unsupported message role: ${String(msg.role)}`);
|
|
339
351
|
}
|
package/src/cli/account-api.ts
CHANGED
|
@@ -18,10 +18,16 @@ export interface AccountRow {
|
|
|
18
18
|
id: string;
|
|
19
19
|
label?: string;
|
|
20
20
|
email?: string;
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Subscription tier. `null` means this version looked and the provider did not report one;
|
|
23
|
+
* an absent key means the row's surface does not carry a tier at all (#3777).
|
|
24
|
+
*/
|
|
25
|
+
plan?: string | null;
|
|
22
26
|
masked?: string;
|
|
23
27
|
active: boolean;
|
|
24
28
|
needsReauth?: boolean;
|
|
29
|
+
/** Registered credential that is still excluded from routing until validation completes. */
|
|
30
|
+
validationPending?: boolean;
|
|
25
31
|
/** Codex pool selection order, higher used earlier. Absent where ordering does not apply. */
|
|
26
32
|
priority?: number;
|
|
27
33
|
quota?: CodexQuotaDto | null;
|
|
@@ -237,6 +243,7 @@ interface CodexAccountDto {
|
|
|
237
243
|
plan?: string;
|
|
238
244
|
isMain?: boolean;
|
|
239
245
|
needsReauth?: boolean;
|
|
246
|
+
health?: { reason?: string };
|
|
240
247
|
priority?: number;
|
|
241
248
|
quota?: CodexQuotaDto | null;
|
|
242
249
|
quotaRefresh?: unknown;
|
|
@@ -263,10 +270,12 @@ export async function fetchCodexRows(
|
|
|
263
270
|
baseUrl: string,
|
|
264
271
|
forceRefresh = false,
|
|
265
272
|
includeQuota = forceRefresh,
|
|
273
|
+
options: { refreshAction?: boolean } = {},
|
|
266
274
|
): Promise<FamilyRows> {
|
|
267
|
-
const
|
|
275
|
+
const refreshAction = options.refreshAction === true;
|
|
276
|
+
const accountsPath = `/api/codex-auth/accounts${refreshAction ? "/refresh" : forceRefresh ? "?refresh=1" : ""}`;
|
|
268
277
|
const [accountsRes, activeRes] = await Promise.all([
|
|
269
|
-
apiJson(deps, baseUrl, "GET", accountsPath),
|
|
278
|
+
apiJson(deps, baseUrl, refreshAction ? "POST" : "GET", accountsPath),
|
|
270
279
|
apiJson(deps, baseUrl, "GET", "/api/codex-auth/active"),
|
|
271
280
|
]);
|
|
272
281
|
if (accountsRes.status !== 0 && accountsRes.status !== 200) {
|
|
@@ -300,6 +309,7 @@ export async function fetchCodexRows(
|
|
|
300
309
|
plan: a.plan,
|
|
301
310
|
active: a.id === activeId,
|
|
302
311
|
needsReauth: a.needsReauth,
|
|
312
|
+
...(a.health?.reason === "validation_pending" ? { validationPending: true } : {}),
|
|
303
313
|
priority: typeof a.priority === "number" ? a.priority : 0,
|
|
304
314
|
paused: a.paused === true,
|
|
305
315
|
...(includeQuota ? {
|
|
@@ -316,6 +326,8 @@ interface OAuthAccountDto {
|
|
|
316
326
|
email?: string;
|
|
317
327
|
active?: boolean;
|
|
318
328
|
needsReauth?: boolean;
|
|
329
|
+
/** Always sent by the management route; explicitly `null` when the tier is unknown. */
|
|
330
|
+
plan?: string | null;
|
|
319
331
|
quota?: CodexQuotaDto | null;
|
|
320
332
|
quotaUnavailable?: boolean;
|
|
321
333
|
}
|
|
@@ -346,6 +358,9 @@ async function fetchOAuthRows(
|
|
|
346
358
|
email: a.email,
|
|
347
359
|
active: a.active ?? a.id === activeId,
|
|
348
360
|
needsReauth: a.needsReauth,
|
|
361
|
+
// Forward the server's answer verbatim, including `null`. Collapsing null to "absent" here
|
|
362
|
+
// would destroy the one distinction this field exists to make.
|
|
363
|
+
plan: a.plan ?? null,
|
|
349
364
|
...(a.quota !== undefined ? { quota: a.quota } : {}),
|
|
350
365
|
...(a.quotaUnavailable !== undefined ? { quotaUnavailable: a.quotaUnavailable } : {}),
|
|
351
366
|
}));
|
package/src/cli/account-auth.ts
CHANGED
|
@@ -154,7 +154,14 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
154
154
|
{}, deps,
|
|
155
155
|
);
|
|
156
156
|
if (state.status === "done") {
|
|
157
|
-
|
|
157
|
+
if (state.validationPending === true) {
|
|
158
|
+
printData({ ...state, recoveryCommand: "ocx gui", recoveryAction: "After quota recovers, click Refresh quotas in the dashboard Codex account pool." }, wantsJson, [
|
|
159
|
+
"Account registered; validation pending (routing disabled).",
|
|
160
|
+
"After quota recovers, open 'ocx gui' and click Refresh quotas to complete validation.",
|
|
161
|
+
]);
|
|
162
|
+
} else {
|
|
163
|
+
printData({ ...state, modelSelection: modelSelectionNextSteps("openai") }, wantsJson, [`Logged in${state.email ? ` as ${String(state.email)}` : ""}.`, ...modelSelectionGuidance("openai")]);
|
|
164
|
+
}
|
|
158
165
|
if (!wantsJson) warnIfCodexCatalogRefreshPending(state);
|
|
159
166
|
return;
|
|
160
167
|
}
|
|
@@ -264,6 +264,7 @@ function refreshLine(row: FamilyRows["rows"][number]): string {
|
|
|
264
264
|
const quotaText = row.quota ? quotaParts(row.quota).join(" ") : "";
|
|
265
265
|
parts.push(quotaText.length > 0 ? quotaText : "quota: unknown");
|
|
266
266
|
if (row.needsReauth) parts.push("needs-reauth");
|
|
267
|
+
if (row.validationPending) parts.push("validation-pending (routing disabled; open 'ocx gui' and click Refresh quotas after recovery)");
|
|
267
268
|
return parts.filter(Boolean).join(" ");
|
|
268
269
|
}
|
|
269
270
|
|
|
@@ -339,7 +340,7 @@ export async function cmdRefresh(args: string[], deps: AccountDeps): Promise<num
|
|
|
339
340
|
} else console.log(`no quota report available for ${name}`);
|
|
340
341
|
return 0;
|
|
341
342
|
}
|
|
342
|
-
const result = await fetchCodexRows(deps, baseUrl, true);
|
|
343
|
+
const result = await fetchCodexRows(deps, baseUrl, true, true, { refreshAction: true });
|
|
343
344
|
const failed = familyFailure(result, `failed to refresh ${name}`);
|
|
344
345
|
if (failed !== null) return failed;
|
|
345
346
|
if (wantsJson) console.log(JSON.stringify({ accounts: result.rows }, null, 2));
|
package/src/cli/account.ts
CHANGED
|
@@ -99,6 +99,7 @@ function statusText(row: AccountRow): string {
|
|
|
99
99
|
if (row.paused) parts.push("paused");
|
|
100
100
|
if (row.active) parts.push(row.type === "codex" ? "selected" : "active");
|
|
101
101
|
if (row.needsReauth) parts.push("needs-reauth");
|
|
102
|
+
if (row.validationPending) parts.push("validation-pending");
|
|
102
103
|
return parts.join(" ");
|
|
103
104
|
}
|
|
104
105
|
|
package/src/cli/capabilities.ts
CHANGED
|
@@ -219,6 +219,18 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
219
219
|
"`--quota` shows cached Codex windows (including 5h); `--refresh` bypasses the server TTL.",
|
|
220
220
|
],
|
|
221
221
|
},
|
|
222
|
+
{
|
|
223
|
+
command: ["account", "refresh"],
|
|
224
|
+
summary: "Refresh account quotas without model validation; pending Codex accounts require dashboard consent.",
|
|
225
|
+
routes: [
|
|
226
|
+
{ method: "POST", path: "/api/codex-auth/accounts/refresh" },
|
|
227
|
+
{ method: "GET", path: "/api/provider-quotas" },
|
|
228
|
+
],
|
|
229
|
+
flags: [{ name: "--json", value: "boolean", summary: "Emit the refresh result as JSON." }],
|
|
230
|
+
mutates: true,
|
|
231
|
+
json: "payload",
|
|
232
|
+
details: ["CLI/admin-token refreshes only observe usage. After quota recovery, a human must click Refresh quotas in the dashboard to authorize model validation. Do not mint a GUI session to work around this consent boundary."],
|
|
233
|
+
},
|
|
222
234
|
{
|
|
223
235
|
command: ["usage"],
|
|
224
236
|
summary: "Token and estimated-cost report over a time range.",
|
|
@@ -307,12 +319,13 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
307
319
|
},
|
|
308
320
|
{
|
|
309
321
|
command: ["logs"],
|
|
310
|
-
summary: "Recent request log rows, filterable by provider, model, conversation, and status.",
|
|
322
|
+
summary: "Recent request log rows, filterable by provider, model, conversation, account, and status.",
|
|
311
323
|
routes: [{ method: "GET", path: "/api/logs" }],
|
|
312
324
|
flags: [
|
|
313
325
|
{ name: "--provider", value: "string", summary: "Restrict to one provider, matching failover attempts too." },
|
|
314
326
|
{ name: "--model", value: "string", summary: "Restrict to one model id, matching failover attempts too." },
|
|
315
327
|
{ name: "--conversation", value: "string", summary: "Restrict to one conversation id (`--conversationId` is accepted too)." },
|
|
328
|
+
{ name: "--account", value: "string", summary: "Restrict to one account log label (`main`, `p<hex6>`, `o<hex6>`), matching failover attempts too." },
|
|
316
329
|
{ name: "--status", value: "string", summary: "An exact code (429) or a class (5xx)." },
|
|
317
330
|
{ name: "--limit", value: "number", summary: "Row cap; defaults to 200." },
|
|
318
331
|
{ name: "--follow", value: "boolean", summary: "Poll for new rows; add --jsonl to emit JSONL." },
|
|
@@ -324,6 +337,7 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
324
337
|
details: [
|
|
325
338
|
"`--provider` and `--model` both match a failover attempt, so a request is findable by what actually served it, not only by what was asked for.",
|
|
326
339
|
"Rows print `conv=<id>` when the entry carries one, so a conversation filter can be told apart from an empty result.",
|
|
340
|
+
"Rows print `acct=<label>` when the account is known, so an `--account` filter can be told apart from an empty result.",
|
|
327
341
|
"`--follow` deduplicates by row id and cannot be combined with `--json`.",
|
|
328
342
|
],
|
|
329
343
|
},
|
package/src/cli/index.ts
CHANGED
|
@@ -1428,9 +1428,13 @@ async function handleStatus() {
|
|
|
1428
1428
|
}
|
|
1429
1429
|
}
|
|
1430
1430
|
const { collectOAuthHealthEntriesForCli, oauthLoginSummary } = await import("../oauth");
|
|
1431
|
+
const { emailMaskingEnabled } = await import("../lib/privacy");
|
|
1431
1432
|
const { formatOAuthHealthForStatus } = await import("./status-oauth");
|
|
1432
1433
|
console.log(` OAuth logins:`);
|
|
1433
|
-
|
|
1434
|
+
// The operator's own `privacy.maskEmails` decision applies to the CLI too: `ocx status` is not
|
|
1435
|
+
// the dashboard, but it reads the same stored addresses, and a flag that only moved one of the
|
|
1436
|
+
// two would leave the operator unable to tell which surface they had configured.
|
|
1437
|
+
for (const e of oauthLoginSummary(emailMaskingEnabled(loadConfig()))) {
|
|
1434
1438
|
console.log(` ${e.provider.padEnd(10)} ${e.loggedIn ? `✓ logged in${e.email ? ` (${e.email})` : ""}` : "✗ not logged in"}`);
|
|
1435
1439
|
}
|
|
1436
1440
|
const oauthHealthBlock = formatOAuthHealthForStatus(await collectOAuthHealthEntriesForCli());
|
|
@@ -21,7 +21,7 @@ import { MAX_COST4_RATE } from "../usage/expected-prices";
|
|
|
21
21
|
import { isValidCost4Rate } from "../usage/user-cost-overlays";
|
|
22
22
|
|
|
23
23
|
const USAGE = `Usage:
|
|
24
|
-
ocx models live [--provider <name>] [--json]
|
|
24
|
+
ocx models live [--provider <name>] [--free-only] [--json]
|
|
25
25
|
ocx models price <provider/model> [--json]
|
|
26
26
|
ocx models set-price <provider/model> --input N --output N [--cache-read N] [--cache-write N] [--json]
|
|
27
27
|
ocx models set-price <provider/model> --auto [--json]
|
|
@@ -52,17 +52,22 @@ type ModelRow = {
|
|
|
52
52
|
custom?: boolean;
|
|
53
53
|
customId?: string;
|
|
54
54
|
displayName?: string;
|
|
55
|
+
pricingStatus?: "free" | "paid";
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
async function live(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
58
59
|
const args = [...argv];
|
|
59
60
|
const wantsJson = takeFlag(args, "--json");
|
|
60
61
|
const provider = takeOption(args, "--provider");
|
|
62
|
+
// Absent pricingStatus means the provider published no usable per-token pair, so it is
|
|
63
|
+
// excluded here for the same fail-closed reason the classifier omits it (#3666).
|
|
64
|
+
const freeOnly = takeFlag(args, "--free-only");
|
|
61
65
|
rejectArgs(args, USAGE);
|
|
62
66
|
const rows = await runtimeRequest<ModelRow[]>("/api/models", {}, deps);
|
|
63
|
-
const
|
|
67
|
+
const byProvider = provider ? rows.filter(row => row.provider === provider) : rows;
|
|
68
|
+
const filtered = freeOnly ? byProvider.filter(row => row.pricingStatus === "free") : byProvider;
|
|
64
69
|
printData(filtered, wantsJson, filtered.map(row => {
|
|
65
|
-
const flags = [row.native ? "native" : "routed", row.custom ? "custom" : "", row.initialSelectionPending ? "initial discovery pending" : row.disabled ? "disabled" : "enabled"].filter(Boolean);
|
|
70
|
+
const flags = [row.native ? "native" : "routed", row.custom ? "custom" : "", row.pricingStatus === "free" ? "free" : "", row.initialSelectionPending ? "initial discovery pending" : row.disabled ? "disabled" : "enabled"].filter(Boolean);
|
|
66
71
|
return `${row.namespaced ?? `${row.provider}/${row.id}`} [${flags.join(", ")}]`;
|
|
67
72
|
}));
|
|
68
73
|
}
|
package/src/cli/observe.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { redactSecretString } from "../lib/redact";
|
|
|
17
17
|
|
|
18
18
|
const USAGE = `Usage:
|
|
19
19
|
ocx observe logs [--provider <name>] [--model <id>] [--status <code>]
|
|
20
|
-
[--conversation <id>] [--limit <n>] [--follow] [--json|--jsonl]
|
|
20
|
+
[--conversation <id>] [--account <label>] [--limit <n>] [--follow] [--json|--jsonl]
|
|
21
21
|
ocx logs explain <request-id> [--json]
|
|
22
22
|
ocx logs rebuild-index
|
|
23
23
|
ocx logs index-status
|
|
@@ -58,7 +58,14 @@ function formatLog(row: LogEntry): string {
|
|
|
58
58
|
const conversation = typeof row.conversationId === "string" && row.conversationId.length > 0
|
|
59
59
|
? `conv=${row.conversationId}`
|
|
60
60
|
: "";
|
|
61
|
-
|
|
61
|
+
// The account label is printed for the same reason, and for one more: it is the answer to
|
|
62
|
+
// "which of my accounts served this?" (#4057). It is only ever the stable non-PII label the
|
|
63
|
+
// proxy already persists (`main`, `p<hex6>`, `o<hex6>`) — never an email, a key, or an
|
|
64
|
+
// upstream account id. Rows from a single-account provider carry no label and print none.
|
|
65
|
+
const account = typeof row.accountLogLabel === "string" && row.accountLogLabel.length > 0
|
|
66
|
+
? `acct=${row.accountLogLabel}`
|
|
67
|
+
: "";
|
|
68
|
+
return [time, String(status), route, duration, account, conversation].filter(Boolean).join(" ");
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
async function logs(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
@@ -72,6 +79,9 @@ async function logs(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
72
79
|
// Both spellings, because the server accepts both (`request-log.ts:1032`) and an operator
|
|
73
80
|
// should not have to remember which one this surface wanted.
|
|
74
81
|
const conversationId = takeOption(args, "--conversation") ?? takeOption(args, "--conversationId");
|
|
82
|
+
// Server-side, so `--limit` caps the rows that MATCHED rather than the rows scanned; a
|
|
83
|
+
// client-side filter after a 200-row cap would silently hide older matches.
|
|
84
|
+
const account = takeOption(args, "--account");
|
|
75
85
|
const limit = takeIntegerOption(args, "--limit", { min: 1 }) ?? 200;
|
|
76
86
|
rejectArgs(args, USAGE);
|
|
77
87
|
if (wantsJson && wantsJsonl) throw new CliUsageError("--json and --jsonl cannot be combined", USAGE);
|
|
@@ -80,7 +90,7 @@ async function logs(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
80
90
|
}
|
|
81
91
|
let seen = new Set<string>();
|
|
82
92
|
do {
|
|
83
|
-
const data = await runtimeRequest(`/api/logs${query({ provider, model, status, conversationId, limit })}`, {}, deps);
|
|
93
|
+
const data = await runtimeRequest(`/api/logs${query({ provider, model, status, conversationId, account, limit })}`, {}, deps);
|
|
84
94
|
const rows = logRows(data);
|
|
85
95
|
if (!follow && wantsJson) printData(data, true);
|
|
86
96
|
else {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// ZCode config export.
|
|
2
2
|
import type { ExportContext, ManagedContribution } from "./contracts";
|
|
3
3
|
import { normalizeExportModels, inputModalitiesForClient, exportModelLabel, authoritativeContextWindow, singleFragment } from "./model-metadata";
|
|
4
|
+
import { sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
4
5
|
import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants";
|
|
5
6
|
|
|
6
7
|
|
|
@@ -15,6 +16,16 @@ export interface ZcodeModelEntry {
|
|
|
15
16
|
name?: string;
|
|
16
17
|
limit?: { context: number; output?: number };
|
|
17
18
|
modalities: { input: string[]; output: string[] };
|
|
19
|
+
/**
|
|
20
|
+
* On-disk Thought Level block. ZCode 3.x persists `variants`/`defaultVariant`
|
|
21
|
+
* and parses them into in-memory `levels`/`defaultLevel`. Omit the field when
|
|
22
|
+
* the catalog has no selectable ladder, so the picker stays hidden.
|
|
23
|
+
*/
|
|
24
|
+
reasoning?: {
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
variants: string[];
|
|
27
|
+
defaultVariant?: string;
|
|
28
|
+
};
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
export interface ZcodeProviderBlock {
|
|
@@ -61,6 +72,19 @@ export function buildZcodeClientConfig(ctx: ExportContext): ZcodeGeneratedConfig
|
|
|
61
72
|
if (context !== undefined) {
|
|
62
73
|
entry.limit = { context };
|
|
63
74
|
}
|
|
75
|
+
// `none` is a Codex omit-sentinel, not a ZCode picker option. Keep catalog
|
|
76
|
+
// `ultra` when present: ZCode forwards the selected variant as
|
|
77
|
+
// `reasoning_effort`. Set `defaultVariant` only when it survives that filter.
|
|
78
|
+
const efforts = sanitizeCodexReasoningEfforts(model.reasoningEfforts)
|
|
79
|
+
?.filter(effort => effort !== "none");
|
|
80
|
+
if (efforts && efforts.length > 0) {
|
|
81
|
+
const defaultVariant = model.defaultReasoningEffort?.trim().toLowerCase();
|
|
82
|
+
entry.reasoning = {
|
|
83
|
+
enabled: true,
|
|
84
|
+
variants: efforts,
|
|
85
|
+
...(defaultVariant && efforts.includes(defaultVariant) ? { defaultVariant } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
64
88
|
models[model.namespaced] = entry;
|
|
65
89
|
}
|
|
66
90
|
return {
|
|
@@ -60,6 +60,11 @@ export function isAccountNeedsReauth(id: string): boolean {
|
|
|
60
60
|
return true;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
export function clearAccountNeedsReauth(id: string): void {
|
|
63
|
+
export function clearAccountNeedsReauth(id: string, credentialGeneration?: number): void {
|
|
64
|
+
// A model response proves only the credential it used. Keep account-wide
|
|
65
|
+
// quarantine and evidence from another generation intact.
|
|
66
|
+
if (credentialGeneration !== undefined
|
|
67
|
+
&& (reauthAccounts.get(id) !== credentialGeneration
|
|
68
|
+
|| !isCodexAccountGenerationLive(id, credentialGeneration))) return;
|
|
64
69
|
reauthAccounts.delete(id);
|
|
65
70
|
}
|
|
@@ -58,7 +58,9 @@ function isCredentialRecord(value: unknown): value is CodexAccountCredentialReco
|
|
|
58
58
|
&& (value.replacedAt === undefined || typeof value.replacedAt === "number")
|
|
59
59
|
&& (value.lastCodexValidatedAt === undefined || typeof value.lastCodexValidatedAt === "number")
|
|
60
60
|
&& (value.lastCodexValidationStatus === undefined || value.lastCodexValidationStatus === "ok" || value.lastCodexValidationStatus === "failed")
|
|
61
|
-
&& (value.lastCodexValidationError === undefined || typeof value.lastCodexValidationError === "string")
|
|
61
|
+
&& (value.lastCodexValidationError === undefined || typeof value.lastCodexValidationError === "string")
|
|
62
|
+
&& (value.codexValidationPending === undefined || typeof value.codexValidationPending === "boolean")
|
|
63
|
+
&& (value.lastCodexValidationTerminal === undefined || typeof value.lastCodexValidationTerminal === "boolean");
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export function refreshGrantFingerprintForToken(refreshToken: string): string {
|
|
@@ -118,11 +120,21 @@ function persistCredentialMutation(store: CodexAccountStore): void {
|
|
|
118
120
|
advanceCodexCredentialMutationEpoch();
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Validation metadata that survives a credential write.
|
|
125
|
+
*
|
|
126
|
+
* `lastCodexValidationTerminal` is deliberately NOT in this list. Every credential write —
|
|
127
|
+
* re-login, the CAS refresh commit, same-grant alias propagation — rebuilds the record from this
|
|
128
|
+
* pick list, so leaving the marker out is what makes a successful refresh or a re-authentication
|
|
129
|
+
* erase a terminal verdict. Both events disprove "the grant was revoked", and a verdict that
|
|
130
|
+
* could only ever be set would brand an account dead forever on one spurious `invalid_grant`.
|
|
131
|
+
*/
|
|
121
132
|
function preservedValidationMetadata(record: CodexAccountCredentialRecord | undefined): Pick<
|
|
122
133
|
CodexAccountCredentialRecord,
|
|
123
|
-
"lastCodexValidatedAt" | "lastCodexValidationStatus" | "lastCodexValidationError"
|
|
134
|
+
"lastCodexValidatedAt" | "lastCodexValidationStatus" | "lastCodexValidationError" | "codexValidationPending"
|
|
124
135
|
> {
|
|
125
136
|
return {
|
|
137
|
+
...(record?.codexValidationPending === true ? { codexValidationPending: true } : {}),
|
|
126
138
|
...(record?.lastCodexValidatedAt !== undefined ? { lastCodexValidatedAt: record.lastCodexValidatedAt } : {}),
|
|
127
139
|
...(record?.lastCodexValidationStatus !== undefined ? { lastCodexValidationStatus: record.lastCodexValidationStatus } : {}),
|
|
128
140
|
...(record?.lastCodexValidationError !== undefined ? { lastCodexValidationError: record.lastCodexValidationError } : {}),
|
|
@@ -135,8 +147,12 @@ export function getCodexAccountCredential(id: string): CodexAccountCredentials |
|
|
|
135
147
|
return record.credential ?? null;
|
|
136
148
|
}
|
|
137
149
|
|
|
138
|
-
export function saveCodexAccountCredential(
|
|
139
|
-
|
|
150
|
+
export function saveCodexAccountCredential(
|
|
151
|
+
id: string,
|
|
152
|
+
cred: CodexAccountCredentials,
|
|
153
|
+
options: { validationPending?: boolean } = {},
|
|
154
|
+
): number {
|
|
155
|
+
return withCredentialMutationLockSync(() => {
|
|
140
156
|
const store = loadCodexAccountRecordStore();
|
|
141
157
|
const current = store[id];
|
|
142
158
|
const refreshGrantFingerprint = current?.credential?.refreshToken === cred.refreshToken
|
|
@@ -148,37 +164,84 @@ export function saveCodexAccountCredential(id: string, cred: CodexAccountCredent
|
|
|
148
164
|
refreshGrantFingerprint,
|
|
149
165
|
replacedAt: current ? Date.now() : undefined,
|
|
150
166
|
...preservedValidationMetadata(current),
|
|
167
|
+
...(options.validationPending ? {
|
|
168
|
+
codexValidationPending: true,
|
|
169
|
+
lastCodexValidatedAt: undefined,
|
|
170
|
+
lastCodexValidationStatus: undefined,
|
|
171
|
+
lastCodexValidationError: undefined,
|
|
172
|
+
} : {}),
|
|
151
173
|
};
|
|
152
174
|
persistCredentialMutation(store);
|
|
175
|
+
return store[id].generation;
|
|
153
176
|
});
|
|
154
177
|
}
|
|
155
178
|
|
|
156
|
-
export function markCodexAccountValidated(id: string, atMs: number = Date.now()): void {
|
|
179
|
+
export function markCodexAccountValidated(id: string, atMs: number = Date.now(), generation?: number): void {
|
|
157
180
|
withCredentialMutationLockSync(() => {
|
|
158
181
|
const store = loadCodexAccountRecordStore();
|
|
159
182
|
const current = store[id];
|
|
160
183
|
if (!current || current.deletedAt != null || !current.credential) return;
|
|
184
|
+
if (current.codexValidationPending && generation === undefined) return;
|
|
185
|
+
if (generation !== undefined && current.generation !== generation) return;
|
|
161
186
|
store[id] = {
|
|
162
187
|
...current,
|
|
163
188
|
lastCodexValidatedAt: atMs,
|
|
164
189
|
lastCodexValidationStatus: "ok",
|
|
165
190
|
lastCodexValidationError: undefined,
|
|
191
|
+
codexValidationPending: undefined,
|
|
192
|
+
// A completed validation is the direct refutation of a terminal verdict, and this
|
|
193
|
+
// spread would otherwise carry the old marker forward.
|
|
194
|
+
lastCodexValidationTerminal: undefined,
|
|
166
195
|
};
|
|
167
|
-
|
|
196
|
+
// Becoming routable invalidates credential-derived caches; a timestamp-only
|
|
197
|
+
// update on an already validated account preserves the existing epoch policy.
|
|
198
|
+
if (current.codexValidationPending) persistCredentialMutation(store);
|
|
199
|
+
else persist(store);
|
|
168
200
|
});
|
|
169
201
|
}
|
|
170
202
|
|
|
171
|
-
export
|
|
172
|
-
|
|
203
|
+
export interface MarkCodexAccountValidationFailedOptions {
|
|
204
|
+
/**
|
|
205
|
+
* Write only while the stored record is still at this generation.
|
|
206
|
+
*
|
|
207
|
+
* A validation attempt is not atomic with the store: an operator can re-authenticate the
|
|
208
|
+
* account, or another writer can commit a refresh, while a probe is still in flight. Without
|
|
209
|
+
* this fence the late failure lands on whatever credential happens to be there and brands a
|
|
210
|
+
* freshly installed one dead. Declining to write is the safe direction — the failure cannot be
|
|
211
|
+
* attributed to a credential the caller never observed.
|
|
212
|
+
*/
|
|
213
|
+
expectedGeneration?: number;
|
|
214
|
+
/** The grant itself is revoked or expired; only a re-login clears it. */
|
|
215
|
+
terminal?: boolean;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Returns whether the verdict was actually persisted (false when the fence declined it). */
|
|
219
|
+
export function markCodexAccountValidationFailed(
|
|
220
|
+
id: string,
|
|
221
|
+
reason: string,
|
|
222
|
+
options: MarkCodexAccountValidationFailedOptions = {},
|
|
223
|
+
): boolean {
|
|
224
|
+
return withCredentialMutationLockSync(() => {
|
|
173
225
|
const store = loadCodexAccountRecordStore();
|
|
174
226
|
const current = store[id];
|
|
175
|
-
if (!current || current.deletedAt != null || !current.credential) return;
|
|
227
|
+
if (!current || current.deletedAt != null || !current.credential) return false;
|
|
228
|
+
// Deferred validation is settled only by a caller that names the generation it observed.
|
|
229
|
+
// An unfenced write must never resolve a pending account, whichever verdict it carries.
|
|
230
|
+
if (current.codexValidationPending && options.expectedGeneration === undefined) return false;
|
|
231
|
+
if (options.expectedGeneration !== undefined && current.generation !== options.expectedGeneration) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
176
234
|
store[id] = {
|
|
177
235
|
...current,
|
|
178
236
|
lastCodexValidationStatus: "failed",
|
|
179
237
|
lastCodexValidationError: reason,
|
|
238
|
+
// Only ever set here. A transient failure must not clear a terminal marker set earlier,
|
|
239
|
+
// and it must not invent one either, so the flag is written only when the caller proves
|
|
240
|
+
// the grant is dead.
|
|
241
|
+
...(options.terminal ? { lastCodexValidationTerminal: true } : {}),
|
|
180
242
|
};
|
|
181
243
|
persist(store);
|
|
244
|
+
return true;
|
|
182
245
|
});
|
|
183
246
|
}
|
|
184
247
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readCodexAccountRecord } from "./account-store";
|
|
2
2
|
import { isAccountNeedsReauth } from "./account-runtime-state";
|
|
3
3
|
import {
|
|
4
4
|
MAIN_CODEX_ACCOUNT_ID,
|
|
@@ -48,5 +48,6 @@ export function isCodexAccountUsable(
|
|
|
48
48
|
.some(account => isSelectableCodexPoolAccount(account) && account.id === accountId);
|
|
49
49
|
if (!exists) return false;
|
|
50
50
|
if (isAccountNeedsReauth(accountId)) return false;
|
|
51
|
-
|
|
51
|
+
const record = readCodexAccountRecord(accountId);
|
|
52
|
+
return !!record?.credential && record.deletedAt == null && !record.codexValidationPending;
|
|
52
53
|
}
|