@yansigit/opencodex 2.36.1-dev.20260829.48 → 2.36.1
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/gui/dist/assets/{ApiKeys-LOOFiZfv.js → ApiKeys-CsmNyf4I.js} +1 -1
- package/gui/dist/assets/{Claude-BwKzpXe3.js → Claude-B4sHsGTD.js} +1 -1
- package/gui/dist/assets/{CodexSet-B5qi9KeE.js → CodexSet-BuAQ4YQn.js} +1 -1
- package/gui/dist/assets/{FileIntegrationPage-B0sUef6W.js → FileIntegrationPage-BC65sEaS.js} +1 -1
- package/gui/dist/assets/{Grok-bsb4n-f4.js → Grok-DNJWlISf.js} +1 -1
- package/gui/dist/assets/{Integrations-CSMwFDvM.js → Integrations-vGJshFZK.js} +2 -2
- package/gui/dist/assets/{IntegrationsOverview-CTORdKJA.js → IntegrationsOverview-DYMkDFHE.js} +1 -1
- package/gui/dist/assets/{Logs-BWYbfELf.js → Logs-Coc7_RPP.js} +1 -1
- package/gui/dist/assets/{Models-CexEtdT1.js → Models-CKVT8Meb.js} +1 -1
- package/gui/dist/assets/{NumberStepper-BapSFQnW.js → NumberStepper-Dug4wR8n.js} +1 -1
- package/gui/dist/assets/{Providers-BwUFeAgA.js → Providers-DyaCHRWL.js} +1 -1
- package/gui/dist/assets/{RestoreDialog-CD8piq90.js → RestoreDialog-BpuvVU2w.js} +1 -1
- package/gui/dist/assets/{Startup-BgX731C2.js → Startup-DAUQqNEN.js} +1 -1
- package/gui/dist/assets/{Storage-Bgq7HphP.js → Storage-DfP95SlJ.js} +1 -1
- package/gui/dist/assets/{Subagents-ChxItbeF.js → Subagents-CMNsZLfk.js} +1 -1
- package/gui/dist/assets/{Usage-BkylzP50.js → Usage-ESFRfBMO.js} +1 -1
- package/gui/dist/assets/{codex-stale-banner-D4gvSWnO.js → codex-stale-banner-W4FhikN7.js} +1 -1
- package/gui/dist/assets/{data-surface-BtO3lwam.js → data-surface-B8PX8rjm.js} +1 -1
- package/gui/dist/assets/{data-surface-pCXChiBf.js → data-surface-CETFXqiA.js} +1 -1
- package/gui/dist/assets/{index-CNopOid3.js → index-DHQHBRhb.js} +3 -3
- package/gui/dist/assets/{model-display-CiUpg8T9.js → model-display-OBTJhQwT.js} +1 -1
- package/gui/dist/assets/{provider-payload-b2jlS-On.js → provider-payload-CJRJUn7w.js} +1 -1
- package/gui/dist/assets/{section-tabs-BP7gEPK6.js → section-tabs-B8sLMiz6.js} +1 -1
- package/gui/dist/assets/shared-DI18-uF7.js +69 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/base.ts +26 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +34 -41
- package/src/adapters/cursor/envelope-echo.ts +128 -0
- package/src/adapters/cursor/request-builder.ts +19 -12
- package/src/adapters/cursor/tool-definitions.ts +2 -1
- package/src/adapters/cursor/tool-result-normalize.ts +23 -31
- package/src/adapters/cursor.ts +21 -2
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +71 -2
- package/src/adapters/google.ts +19 -4
- package/src/adapters/kiro-constants.ts +12 -0
- package/src/adapters/kiro.ts +128 -11
- package/src/adapters/openai-chat.ts +16 -2
- package/src/adapters/openai-responses.ts +15 -2
- package/src/adapters/tool-catalog-nudge.ts +2 -1
- package/src/adapters/xai-web-search.ts +10 -14
- package/src/claude/outbound.ts +14 -3
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +93 -16
- package/src/cli/account-extended.ts +262 -36
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +40 -10
- package/src/cli/agent.ts +8 -1
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +535 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/dispatch.ts +195 -27
- package/src/cli/doctor.ts +100 -1
- package/src/cli/help.ts +11 -2
- package/src/cli/index.ts +19 -3
- package/src/cli/inspect.ts +230 -0
- package/src/cli/observe.ts +11 -3
- package/src/cli/registry.ts +34 -2
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +16 -0
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/usage-report.ts +52 -2
- package/src/cli/version-skew.ts +46 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/catalog/provider-fetch.ts +4 -0
- package/src/codex/transition-state.ts +12 -3
- package/src/compatibility/openai-responses.ts +9 -1
- package/src/generated/compatibility-version.json +94 -58
- package/src/integrations/ownership-policy.ts +24 -5
- package/src/integrations/ownership.ts +36 -2
- package/src/integrations/state.ts +40 -7
- package/src/integrations/writer.ts +21 -3
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/errors.ts +25 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/oauth/store.ts +16 -6
- package/src/providers/label.ts +34 -1
- package/src/responses/turn-termination.ts +107 -0
- package/src/server/management/logs-usage-routes.ts +5 -27
- package/src/server/management/route-registry.ts +317 -0
- package/src/server/proxy-liveness.ts +27 -4
- package/src/server/request-log.ts +37 -17
- package/src/server/responses/core.ts +80 -4
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/service.ts +34 -0
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +79 -33
- package/src/usage/log.ts +32 -6
- package/src/usage/summary.ts +22 -34
- package/gui/dist/assets/shared-BdYGV-yJ.js +0 -69
|
@@ -12,10 +12,11 @@ import { ClientPathError, EXPORT_CLIENTS, opencodeProxyBaseUrl, type ExportModel
|
|
|
12
12
|
import type { OcxConfig } from "../types";
|
|
13
13
|
import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
14
14
|
import { SNAPSHOT_RETENTION } from "./journal";
|
|
15
|
-
import { canonicalContribution, fingerprint, type OwnershipRecord } from "./ownership";
|
|
15
|
+
import { canonicalContribution, fingerprint, semanticContribution, type OwnershipRecord } from "./ownership";
|
|
16
16
|
import {
|
|
17
17
|
protectedContributionFingerprint,
|
|
18
18
|
refreshablePathsOf,
|
|
19
|
+
semanticProtectedContributionFingerprint,
|
|
19
20
|
validRefreshablePaths,
|
|
20
21
|
} from "./ownership-policy";
|
|
21
22
|
import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
|
|
@@ -153,21 +154,49 @@ function recordedBlockIsOwned(
|
|
|
153
154
|
if (!observed) return false;
|
|
154
155
|
if (fingerprint(canonicalContribution(observed)) === record.blockFingerprint) return true;
|
|
155
156
|
|
|
157
|
+
const observedSemanticFingerprint = fingerprint(semanticContribution(observed));
|
|
158
|
+
if (
|
|
159
|
+
typeof record.semanticBlockFingerprint === "string"
|
|
160
|
+
&& observedSemanticFingerprint === record.semanticBlockFingerprint
|
|
161
|
+
) return true;
|
|
162
|
+
|
|
163
|
+
const desiredFingerprint = fingerprint(canonicalContribution(desired));
|
|
164
|
+
if (
|
|
165
|
+
desiredFingerprint === record.blockFingerprint
|
|
166
|
+
&& observedSemanticFingerprint === fingerprint(semanticContribution(desired))
|
|
167
|
+
) return true;
|
|
168
|
+
|
|
156
169
|
if (
|
|
157
170
|
typeof record.protectedBlockFingerprint === "string"
|
|
158
171
|
&& validRefreshablePaths(observed, record.refreshablePaths)
|
|
159
172
|
&& record.refreshablePaths.length > 0
|
|
160
173
|
) {
|
|
161
|
-
|
|
162
|
-
|
|
174
|
+
const observedProtectedFingerprint = protectedContributionFingerprint(
|
|
175
|
+
observed,
|
|
176
|
+
record.refreshablePaths,
|
|
177
|
+
);
|
|
178
|
+
if (observedProtectedFingerprint === record.protectedBlockFingerprint) return true;
|
|
179
|
+
|
|
180
|
+
const observedSemanticProtectedFingerprint = semanticProtectedContributionFingerprint(
|
|
181
|
+
observed,
|
|
182
|
+
record.refreshablePaths,
|
|
183
|
+
);
|
|
184
|
+
if (
|
|
185
|
+
typeof record.semanticProtectedBlockFingerprint === "string"
|
|
186
|
+
&& observedSemanticProtectedFingerprint === record.semanticProtectedBlockFingerprint
|
|
187
|
+
) return true;
|
|
188
|
+
|
|
189
|
+
return protectedContributionFingerprint(desired, record.refreshablePaths)
|
|
190
|
+
=== record.protectedBlockFingerprint
|
|
191
|
+
&& observedSemanticProtectedFingerprint
|
|
192
|
+
=== semanticProtectedContributionFingerprint(desired, record.refreshablePaths);
|
|
163
193
|
}
|
|
164
194
|
|
|
165
|
-
const desiredFingerprint = fingerprint(canonicalContribution(desired));
|
|
166
195
|
if (desiredFingerprint !== record.blockFingerprint) return false;
|
|
167
196
|
const legacyPaths = refreshablePathsOf(desired);
|
|
168
197
|
return legacyPaths.length > 0
|
|
169
|
-
&&
|
|
170
|
-
===
|
|
198
|
+
&& semanticProtectedContributionFingerprint(observed, legacyPaths)
|
|
199
|
+
=== semanticProtectedContributionFingerprint(desired, legacyPaths);
|
|
171
200
|
}
|
|
172
201
|
|
|
173
202
|
/**
|
|
@@ -257,7 +286,11 @@ export function classifyIntegration(input: {
|
|
|
257
286
|
}
|
|
258
287
|
return { state: "stale" };
|
|
259
288
|
}
|
|
260
|
-
|
|
289
|
+
const desiredFingerprint = typeof input.record.semanticBlockFingerprint === "string"
|
|
290
|
+
? fingerprint(semanticContribution(input.contribution))
|
|
291
|
+
: fingerprint(canonicalContribution(input.contribution));
|
|
292
|
+
const recordedFingerprint = input.record.semanticBlockFingerprint ?? input.record.blockFingerprint;
|
|
293
|
+
return recordedFingerprint === desiredFingerprint
|
|
261
294
|
? { state: "current" }
|
|
262
295
|
: { state: "stale" };
|
|
263
296
|
}
|
|
@@ -15,8 +15,18 @@ import { EXPORT_CLIENTS, type ExportModel, type ManagedContribution } from "../c
|
|
|
15
15
|
import { isLoopbackHostname } from "../codex/inject";
|
|
16
16
|
import type { OcxConfig } from "../types";
|
|
17
17
|
import { PARSE_FAILED, defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
fingerprint,
|
|
20
|
+
canonicalContribution,
|
|
21
|
+
fragmentPathsOf,
|
|
22
|
+
semanticContribution,
|
|
23
|
+
type OwnershipRecord,
|
|
24
|
+
} from "./ownership";
|
|
25
|
+
import {
|
|
26
|
+
protectedContributionFingerprint,
|
|
27
|
+
refreshablePathsOf,
|
|
28
|
+
semanticProtectedContributionFingerprint,
|
|
29
|
+
} from "./ownership-policy";
|
|
20
30
|
import { createdContainerPaths, mergeContribution, removeFragments } from "./merge";
|
|
21
31
|
import { INTEGRATION_CLIENTS, isLoopbackOnly, type IntegrationClientId } from "./registry";
|
|
22
32
|
import { classifyIntegration, exportContextOf } from "./state";
|
|
@@ -361,8 +371,13 @@ function applyOrRefreshIntegration(input: IntegrationWriteInput, allowAbsent: bo
|
|
|
361
371
|
record: {
|
|
362
372
|
clientId, configPath, fileFingerprint: fingerprint(text),
|
|
363
373
|
blockFingerprint: fingerprint(canonicalContribution(contribution)),
|
|
374
|
+
semanticBlockFingerprint: fingerprint(semanticContribution(contribution)),
|
|
364
375
|
...(refreshablePaths.length > 0 ? {
|
|
365
376
|
protectedBlockFingerprint: protectedContributionFingerprint(contribution, refreshablePaths),
|
|
377
|
+
semanticProtectedBlockFingerprint: semanticProtectedContributionFingerprint(
|
|
378
|
+
contribution,
|
|
379
|
+
refreshablePaths,
|
|
380
|
+
),
|
|
366
381
|
refreshablePaths,
|
|
367
382
|
} : {}),
|
|
368
383
|
fragmentPaths: fragmentPathsOf(contribution), createdContainers: created,
|
|
@@ -556,7 +571,10 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
556
571
|
? (restoredText === null ? "absent" : "conflict")
|
|
557
572
|
: !recordDescribesBytes
|
|
558
573
|
? "conflict"
|
|
559
|
-
:
|
|
574
|
+
: (
|
|
575
|
+
restoredRecord.semanticBlockFingerprint === fingerprint(semanticContribution(fresh))
|
|
576
|
+
|| restoredRecord.blockFingerprint === fingerprint(canonicalContribution(fresh))
|
|
577
|
+
)
|
|
560
578
|
? "current"
|
|
561
579
|
: "stale";
|
|
562
580
|
|
package/src/lib/admin-secrets.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
1
2
|
import { lstatSync, readFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { getConfigDir } from "../config";
|
|
@@ -23,3 +24,26 @@ export function loadAdminTokenFromFile(configDir = getConfigDir()): string | nul
|
|
|
23
24
|
export function configuredAdminToken(configDir = getConfigDir(), env: NodeJS.ProcessEnv = process.env): string | null {
|
|
24
25
|
return env.OPENCODEX_ADMIN_AUTH_TOKEN?.trim() || loadAdminTokenFromFile(configDir);
|
|
25
26
|
}
|
|
27
|
+
|
|
28
|
+
export const ADMIN_TOKEN_PREFIX = "ocx_admin_";
|
|
29
|
+
|
|
30
|
+
function secretTextEquals(left: string, right: string): boolean {
|
|
31
|
+
const a = Buffer.from(left);
|
|
32
|
+
const b = Buffer.from(right);
|
|
33
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* True when `token` is a management credential: minted `ocx_admin_…` shape, or
|
|
38
|
+
* byte-equal to the configured admin token (env or admin-api-token file).
|
|
39
|
+
* Used by the service write/start chokepoint and by doctor so the two cannot drift.
|
|
40
|
+
*/
|
|
41
|
+
export function tokenCollidesWithAdmin(
|
|
42
|
+
token: string,
|
|
43
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
44
|
+
configDir = getConfigDir(),
|
|
45
|
+
): boolean {
|
|
46
|
+
if (token.startsWith(ADMIN_TOKEN_PREFIX)) return true;
|
|
47
|
+
const admin = configuredAdminToken(configDir, env);
|
|
48
|
+
return admin !== null && secretTextEquals(token, admin);
|
|
49
|
+
}
|
package/src/lib/errors.ts
CHANGED
|
@@ -349,6 +349,12 @@ export function inferHttpStatusFromAdapterMessage(message: string): number {
|
|
|
349
349
|
// subscription/permission wording.
|
|
350
350
|
if (isAuthenticationMessage(lower)) return 401;
|
|
351
351
|
if (isSubscriptionGateMessage(lower) || isPermissionMessage(lower)) return 403;
|
|
352
|
+
// Same precedence rule as classifyCursorError: an explicit gRPC FAILED_PRECONDITION is a
|
|
353
|
+
// structured, deterministic rejection, so it outranks the overload keywords that routinely
|
|
354
|
+
// appear beside it ("failed_precondition: model unavailable for this plan"). Without this,
|
|
355
|
+
// the message matched "unavailable" and returned a retryable 503, so clients kept retrying
|
|
356
|
+
// a rejection that can never succeed.
|
|
357
|
+
if (lower.includes("failed_precondition") || lower.includes("failed precondition")) return 400;
|
|
352
358
|
if (
|
|
353
359
|
lower.includes("unavailable") ||
|
|
354
360
|
lower.includes("overloaded") ||
|
|
@@ -424,6 +430,24 @@ export function httpStatusFromTerminalError(error: {
|
|
|
424
430
|
if (message && isClientClosedMessage(message)) return 499;
|
|
425
431
|
if (error.type === "invalid_request_error") return 400;
|
|
426
432
|
if (error.type === "proxy_error") return 500;
|
|
427
|
-
|
|
433
|
+
// A structured server class must not be downgraded to a CLIENT error by message wording.
|
|
434
|
+
// classifyError assigns `server_error` + `upstream_server_error` to every 5xx it sees, so
|
|
435
|
+
// the class is authoritative about blame: the upstream failed, the caller did not send a
|
|
436
|
+
// bad request. What it is NOT authoritative about is which server status fits — a stall is
|
|
437
|
+
// genuinely 504 and an overload genuinely 503, and flattening those to 502 discards
|
|
438
|
+
// information both the log surface and the retry policy read. So message inference still
|
|
439
|
+
// chooses the specific status, and only a client-error verdict is overridden.
|
|
440
|
+
//
|
|
441
|
+
// The override is deliberately narrowed to 400 alone. 429, 499, 401 and 403 are all
|
|
442
|
+
// actionable signals the caller routes on — retry-after, client cancellation, re-auth,
|
|
443
|
+
// entitlement — and overriding them would trade one kind of misreport for another. 400 is
|
|
444
|
+
// the single verdict that both blames the caller and stops the retry, which is the failure
|
|
445
|
+
// being fixed: an upstream 500 whose text happens to contain "malformed" or "invalid
|
|
446
|
+
// request" used to return 400, so Claude Code stopped retrying a retryable failure.
|
|
447
|
+
const structuredServerClass = error.type === "server_error" || error.code === "upstream_server_error";
|
|
448
|
+
if (message) {
|
|
449
|
+
const inferred = inferHttpStatusFromAdapterMessage(message);
|
|
450
|
+
return structuredServerClass && inferred === 400 ? 502 : inferred;
|
|
451
|
+
}
|
|
428
452
|
return 502;
|
|
429
453
|
}
|
|
@@ -23,3 +23,18 @@ export function loadServiceTokenFromFile(env: Record<string, string | undefined>
|
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Contents of the installed service token file. The launch wrapper always re-exports
|
|
29
|
+
* this file as OPENCODEX_API_AUTH_TOKEN, so doctor and start must inspect it even
|
|
30
|
+
* when the calling shell has no data-plane env var.
|
|
31
|
+
* Returns the token or null — never throws, never logs the value.
|
|
32
|
+
*/
|
|
33
|
+
export function readInstalledServiceToken(): string | null {
|
|
34
|
+
try {
|
|
35
|
+
const token = readFileSync(serviceApiTokenFilePath(), "utf8").trim();
|
|
36
|
+
return token || null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/oauth/store.ts
CHANGED
|
@@ -591,24 +591,34 @@ export async function upsertCredentialByIdentity(
|
|
|
591
591
|
}, [provider, safe]);
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
-
/**
|
|
595
|
-
|
|
594
|
+
/**
|
|
595
|
+
* Remove the ACTIVE account; remaining accounts promote the first one.
|
|
596
|
+
*
|
|
597
|
+
* Returns what actually happened, which a caller cannot otherwise know. A read-then-remove
|
|
598
|
+
* preflight is not equivalent: `mutateStore` serializes mutations, so between a caller's
|
|
599
|
+
* `getAccountSet` check and its `removeCredential` call another process can remove the same
|
|
600
|
+
* account, and both callers would then report a removal that only one of them performed.
|
|
601
|
+
* Deciding inside the mutation is the only place the answer is true when it is returned.
|
|
602
|
+
*/
|
|
603
|
+
export async function removeCredential(provider: string): Promise<"removed" | "not-found"> {
|
|
596
604
|
let removedAccountId: string | undefined;
|
|
597
|
-
await mutateStore(store => {
|
|
605
|
+
const result = await mutateStore(store => {
|
|
598
606
|
const set = store[provider];
|
|
599
|
-
if (!set) return;
|
|
607
|
+
if (!set) return "not-found" as const;
|
|
600
608
|
removedAccountId = set.activeAccountId;
|
|
601
609
|
set.accounts = set.accounts.filter(a => a.id !== set.activeAccountId);
|
|
602
610
|
if (set.accounts.length === 0) {
|
|
603
611
|
delete store[provider];
|
|
604
|
-
return;
|
|
612
|
+
return "removed" as const;
|
|
605
613
|
}
|
|
606
614
|
set.activeAccountId = set.accounts[0]!.id;
|
|
615
|
+
return "removed" as const;
|
|
607
616
|
}, [provider]);
|
|
608
|
-
if (provider === "google-antigravity" && removedAccountId) {
|
|
617
|
+
if (result === "removed" && provider === "google-antigravity" && removedAccountId) {
|
|
609
618
|
const { clearAntigravityRoutingStateForAccount } = await import("./antigravity-routing");
|
|
610
619
|
clearAntigravityRoutingStateForAccount(removedAccountId);
|
|
611
620
|
}
|
|
621
|
+
return result;
|
|
612
622
|
}
|
|
613
623
|
|
|
614
624
|
// ---------------------------------------------------------------------------
|
package/src/providers/label.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
|
|
1
|
+
import { CODEX_ACCOUNT_LOG_LABEL_RE, oauthAccountLogLabel } from "../codex/account-label";
|
|
2
|
+
import type { OcxProviderConfig } from "../types";
|
|
2
3
|
|
|
3
4
|
export function canonicalUsageProviderLabel(provider: string): string {
|
|
4
5
|
return provider === "chatgpt" || provider === "openai-multi" ? "openai" : provider;
|
|
@@ -17,3 +18,35 @@ export function baseProviderLabel(provider: string): string {
|
|
|
17
18
|
if (suffix === "main") return canonicalUsageProviderLabel(provider.slice(0, cut));
|
|
18
19
|
return CODEX_ACCOUNT_LOG_LABEL_RE.test(suffix) ? canonicalUsageProviderLabel(provider.slice(0, cut)) : provider;
|
|
19
20
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Stamp the per-account usage label for a non-Codex OAuth provider (#2699).
|
|
24
|
+
*
|
|
25
|
+
* Call this where the resolved credential is known and NOT inside a failover gate. The obvious
|
|
26
|
+
* place -- next to the `genericFailoverAccountId` assignment in `core.ts` -- sits inside
|
|
27
|
+
* `isGenericFailoverProvider`, and the rotation paths additionally require two or more stored
|
|
28
|
+
* accounts. Stamping there would leave the ordinary case (one xai account, failover off) with no
|
|
29
|
+
* label at all while every test still passed, which is the bug this fixes rather than a variant
|
|
30
|
+
* of it.
|
|
31
|
+
*
|
|
32
|
+
* Two providers are skipped because they already have attribution:
|
|
33
|
+
* - `openai` produces its own `p`-labels through `codexAuthContextLogLabel`.
|
|
34
|
+
* - `anthropic` folds the account into the provider label (`formatAnthropicProviderForLog`).
|
|
35
|
+
*
|
|
36
|
+
* It lives here rather than in `account-label.ts` because it needs `baseProviderLabel`, and this
|
|
37
|
+
* module already imports from that one -- the reverse direction would be an import cycle. This
|
|
38
|
+
* file stays Lab-clean, which matters because `core.ts` is one of the three files
|
|
39
|
+
* `tests/core-lab-boundary.test.ts` guards.
|
|
40
|
+
*/
|
|
41
|
+
export function stampOAuthAccountLabel(
|
|
42
|
+
logCtx: { accountLogLabel?: string },
|
|
43
|
+
providerName: string,
|
|
44
|
+
provider: Pick<OcxProviderConfig, "authMode">,
|
|
45
|
+
accountId: string | undefined,
|
|
46
|
+
): void {
|
|
47
|
+
if (!accountId) return;
|
|
48
|
+
if (provider.authMode !== "oauth") return;
|
|
49
|
+
const base = baseProviderLabel(providerName);
|
|
50
|
+
if (base === "openai" || base === "anthropic") return;
|
|
51
|
+
logCtx.accountLogLabel = oauthAccountLogLabel(accountId, base);
|
|
52
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { OcxAssistantMessage, OcxMessage, OcxParsedRequest } from "../types";
|
|
3
|
+
|
|
4
|
+
const DELIVERED_FINAL_ANSWER_TTL_MS = 60 * 60 * 1_000;
|
|
5
|
+
const DELIVERED_FINAL_ANSWER_MAX_ENTRIES = 1_024;
|
|
6
|
+
|
|
7
|
+
interface DeliveredFinalAnswerRecord {
|
|
8
|
+
fingerprint: string;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const scopesByRequest = new WeakMap<OcxParsedRequest, string>();
|
|
13
|
+
const deliveredFinalAnswers = new Map<string, DeliveredFinalAnswerRecord>();
|
|
14
|
+
|
|
15
|
+
function pruneDeliveredFinalAnswers(at = Date.now()): void {
|
|
16
|
+
for (const [scope, record] of deliveredFinalAnswers) {
|
|
17
|
+
if (at - record.createdAt > DELIVERED_FINAL_ANSWER_TTL_MS) deliveredFinalAnswers.delete(scope);
|
|
18
|
+
}
|
|
19
|
+
while (deliveredFinalAnswers.size > DELIVERED_FINAL_ANSWER_MAX_ENTRIES) {
|
|
20
|
+
const oldest = deliveredFinalAnswers.keys().next().value;
|
|
21
|
+
if (oldest === undefined) break;
|
|
22
|
+
deliveredFinalAnswers.delete(oldest);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function textFingerprint(text: string): string {
|
|
27
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assistantText(message: OcxAssistantMessage): string | undefined {
|
|
31
|
+
if (message.content.some(part => part.type === "toolCall")) return undefined;
|
|
32
|
+
const text = message.content
|
|
33
|
+
.filter((part): part is Extract<typeof part, { type: "text" }> => part.type === "text")
|
|
34
|
+
.map(part => part.text)
|
|
35
|
+
.join("");
|
|
36
|
+
return text.trim().length > 0 ? text : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function deliveredFinalAnswerText(response: unknown): string | undefined {
|
|
40
|
+
if (!response || typeof response !== "object" || Array.isArray(response)) return undefined;
|
|
41
|
+
const output = (response as { output?: unknown }).output;
|
|
42
|
+
if (!Array.isArray(output)) return undefined;
|
|
43
|
+
for (let i = output.length - 1; i >= 0; i--) {
|
|
44
|
+
const item = output[i];
|
|
45
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
46
|
+
const message = item as { type?: unknown; role?: unknown; phase?: unknown; content?: unknown };
|
|
47
|
+
if (message.type !== "message" || message.role !== "assistant" || message.phase !== "final_answer") continue;
|
|
48
|
+
if (!Array.isArray(message.content)) return undefined;
|
|
49
|
+
const text = message.content
|
|
50
|
+
.filter(part => !!part && typeof part === "object" && !Array.isArray(part)
|
|
51
|
+
&& (part as { type?: unknown }).type === "output_text"
|
|
52
|
+
&& typeof (part as { text?: unknown }).text === "string")
|
|
53
|
+
.map(part => (part as { text: string }).text)
|
|
54
|
+
.join("");
|
|
55
|
+
return text.trim().length > 0 ? text : undefined;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Bind the normalized per-conversation digest without adding proxy-private fields to the wire body. */
|
|
61
|
+
export function bindTurnTerminationScope(parsed: OcxParsedRequest, scope: string | undefined): void {
|
|
62
|
+
// Only the normalized log-conversation digest may key this process-wide map. Refusing any raw
|
|
63
|
+
// fallback prevents a future caller from retaining a client header or account identifier here.
|
|
64
|
+
if (!scope || !/^[0-9a-f]{32}$/.test(scope)) return;
|
|
65
|
+
scopesByRequest.set(parsed, scope);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Remember only a final-answer message the proxy actually emitted for this exact conversation. */
|
|
69
|
+
export function rememberDeliveredFinalAnswer(parsed: OcxParsedRequest, response: unknown): void {
|
|
70
|
+
const scope = scopesByRequest.get(parsed);
|
|
71
|
+
if (!scope) return;
|
|
72
|
+
const text = deliveredFinalAnswerText(response);
|
|
73
|
+
if (!text) return;
|
|
74
|
+
const at = Date.now();
|
|
75
|
+
pruneDeliveredFinalAnswers(at);
|
|
76
|
+
// Refresh insertion order so cap eviction removes the least recently delivered conversation.
|
|
77
|
+
deliveredFinalAnswers.delete(scope);
|
|
78
|
+
deliveredFinalAnswers.set(scope, { fingerprint: textFingerprint(text), createdAt: at });
|
|
79
|
+
pruneDeliveredFinalAnswers(at);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Match only when the delivered assistant answer is still the trailing content-bearing message.
|
|
84
|
+
* A later user/tool-result message is new work and must reach the provider, even though every
|
|
85
|
+
* legitimate next turn necessarily contains such a message somewhere in its history.
|
|
86
|
+
*/
|
|
87
|
+
export function hasRecordedTrailingDeliveredFinalAnswer(
|
|
88
|
+
parsed: OcxParsedRequest,
|
|
89
|
+
messages: readonly OcxMessage[],
|
|
90
|
+
): boolean {
|
|
91
|
+
const scope = scopesByRequest.get(parsed);
|
|
92
|
+
if (!scope) return false;
|
|
93
|
+
pruneDeliveredFinalAnswers();
|
|
94
|
+
const record = deliveredFinalAnswers.get(scope);
|
|
95
|
+
if (!record) return false;
|
|
96
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
97
|
+
const message = messages[i];
|
|
98
|
+
if (message.role !== "assistant") return false;
|
|
99
|
+
const text = assistantText(message as OcxAssistantMessage);
|
|
100
|
+
if (text === undefined) {
|
|
101
|
+
if ((message as OcxAssistantMessage).content.some(part => part.type === "toolCall")) return false;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
return textFingerprint(text) === record.fingerprint;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
submitManualLoginCode,
|
|
21
21
|
upsertOAuthProvider,
|
|
22
22
|
} from "../../oauth";
|
|
23
|
-
import {
|
|
23
|
+
import { removeCredential } from "../../oauth/store";
|
|
24
24
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
25
25
|
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
|
|
26
26
|
import { deriveProviderPresets } from "../../providers/derive";
|
|
@@ -31,8 +31,6 @@ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
|
31
31
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
32
32
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
33
33
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
34
|
-
import { resolveCodexHomeDir } from "../../codex/home";
|
|
35
|
-
import { scanStorage } from "../../storage/scanner";
|
|
36
34
|
import { executeArchivedCleanup, listTrashEntries, pickWireCleanupTestHooks, previewArchivedCleanup, type CleanupMode, type RestoreErrorCode } from "../../storage/cleanup";
|
|
37
35
|
import { runArchivedCleanupJob } from "../../storage/cleanup-job";
|
|
38
36
|
import { getRestoreTrashTestStreamResponse, runRestoreTrashEntryJob } from "../../storage/restore-job";
|
|
@@ -198,16 +196,10 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
198
196
|
const filter = {
|
|
199
197
|
provider: url.searchParams.get("provider"),
|
|
200
198
|
model: url.searchParams.get("model"),
|
|
201
|
-
account: url.searchParams.get("account"),
|
|
202
199
|
};
|
|
203
|
-
const fallbackAccounts: Record<string, string> = {};
|
|
204
|
-
for (const p of listOAuthProviders()) {
|
|
205
|
-
const set = getAccountSet(p);
|
|
206
|
-
if (set && set.accounts.length === 1 && set.activeAccountId) fallbackAccounts[p] = set.activeAccountId;
|
|
207
|
-
}
|
|
208
200
|
const project = <T extends UsageSummary>(summary: T, entries?: PersistedUsageEntry[]) =>
|
|
209
|
-
projectUsageSummary(summary, filter, entries
|
|
210
|
-
const filterRequested = Boolean(filter.provider ?? filter.model
|
|
201
|
+
projectUsageSummary(summary, filter, entries);
|
|
202
|
+
const filterRequested = Boolean(filter.provider ?? filter.model);
|
|
211
203
|
const now = Date.now();
|
|
212
204
|
try {
|
|
213
205
|
const cacheKey = `${range}:${surface}`;
|
|
@@ -242,7 +234,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
242
234
|
const revisionReadAt = Date.now();
|
|
243
235
|
const window = snapshotWindow(snapshot.entries);
|
|
244
236
|
const summary = {
|
|
245
|
-
...summarizeUsage(snapshot.entries, range, now, surface
|
|
237
|
+
...summarizeUsage(snapshot.entries, range, now, surface),
|
|
246
238
|
historyTruncated: snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated,
|
|
247
239
|
truncatedPrefixBytes: snapshot.truncatedPrefixBytes,
|
|
248
240
|
entriesTruncated: snapshot.entriesTruncated,
|
|
@@ -270,7 +262,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
270
262
|
for (const nextRange of ranges) {
|
|
271
263
|
for (const nextSurface of surfaces) {
|
|
272
264
|
const nextSummary = nextRange === range && nextSurface === surface ? summary : {
|
|
273
|
-
...summarizeUsage(snapshot.entries, nextRange, now, nextSurface
|
|
265
|
+
...summarizeUsage(snapshot.entries, nextRange, now, nextSurface),
|
|
274
266
|
historyTruncated: summary.historyTruncated,
|
|
275
267
|
truncatedPrefixBytes: summary.truncatedPrefixBytes,
|
|
276
268
|
entriesTruncated: summary.entriesTruncated,
|
|
@@ -343,20 +335,6 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
343
335
|
}
|
|
344
336
|
}
|
|
345
337
|
|
|
346
|
-
if (url.pathname === "/api/storage" && req.method === "GET") {
|
|
347
|
-
try {
|
|
348
|
-
return jsonResponse(scanStorage());
|
|
349
|
-
} catch {
|
|
350
|
-
return jsonResponse({
|
|
351
|
-
codexHome: resolveCodexHomeDir(),
|
|
352
|
-
generatedAt: Date.now(),
|
|
353
|
-
total: { bytes: 0, fileCount: 0 },
|
|
354
|
-
buckets: [],
|
|
355
|
-
error: "scan_failed",
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
338
|
if (url.pathname === "/api/storage/cleanup/preview" && req.method === "POST") {
|
|
361
339
|
let body: { percent?: unknown };
|
|
362
340
|
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
|