@bitkyc08/opencodex 2.44.0 → 2.45.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.
Files changed (38) hide show
  1. package/gui/dist/assets/index-CCfD72yq.js +115 -0
  2. package/gui/dist/assets/index-J96sug5C.css +1 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/openai-responses.ts +14 -0
  6. package/src/chat/outbound.ts +286 -35
  7. package/src/claude/compatibility.ts +192 -0
  8. package/src/claude/model-info.ts +14 -2
  9. package/src/cli/account-extended.ts +24 -1
  10. package/src/cli/init.ts +70 -13
  11. package/src/codex/catalog/provider-fetch.ts +38 -9
  12. package/src/codex/catalog/sync.ts +34 -2
  13. package/src/codex/catalog.ts +1 -1
  14. package/src/config/initialize.ts +132 -0
  15. package/src/config/rebase-provenance.ts +26 -0
  16. package/src/config.ts +50 -1
  17. package/src/generated/compatibility-version.json +41 -29
  18. package/src/lib/windows-secret-acl.ts +8 -4
  19. package/src/providers/quota.ts +26 -15
  20. package/src/responses/state.ts +48 -3
  21. package/src/server/chat-completions.ts +32 -36
  22. package/src/server/chat-native-sse.ts +23 -3
  23. package/src/server/chat-native.ts +15 -8
  24. package/src/server/claude-messages.ts +27 -0
  25. package/src/server/index.ts +5 -1
  26. package/src/server/management/agent-settings-routes.ts +101 -14
  27. package/src/server/management/logs-usage-routes.ts +9 -2
  28. package/src/server/request-log-cursor.ts +84 -0
  29. package/src/server/request-log.ts +46 -3
  30. package/src/server/responses/agent-task-recovery.ts +50 -14
  31. package/src/server/responses/codex-ws-exchange.ts +79 -0
  32. package/src/server/responses/compact.ts +39 -33
  33. package/src/server/responses/core.ts +43 -27
  34. package/src/storage/cleanup.ts +49 -35
  35. package/src/types/config.ts +4 -0
  36. package/src/usage/log.ts +22 -0
  37. package/gui/dist/assets/index-B7_K1Hsj.js +0 -115
  38. package/gui/dist/assets/index-ltx3L-WS.css +0 -1
@@ -321,8 +321,9 @@ import {
321
321
  import {
322
322
  agentTaskRecoveryConfig,
323
323
  discardEncryptedAgentTaskRecovery,
324
- recoverEncryptedAgentTask,
324
+ recoverEncryptedAgentTaskWithResult,
325
325
  restoreCachedEncryptedAgentTasks,
326
+ type AgentTaskRecoveryFailureReason,
326
327
  } from "./agent-task-recovery";
327
328
  import { relaySseEagerBounded } from "../relay-eager";
328
329
  import {
@@ -1534,7 +1535,9 @@ export function codexForwardTerminalOutcomeRecorder(
1534
1535
  ): ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined {
1535
1536
  if (!usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
1536
1537
  return (status, httpStatusOverride) => {
1537
- if (status === "incomplete") {
1538
+ const quotaStatus = [httpStatusOverride, logCtx?.terminalHttpStatus]
1539
+ .find(value => value === 429 || value === 402);
1540
+ if (status === "incomplete" && quotaStatus === undefined) {
1538
1541
  // Normal limit/content-filter/stall terminal — the account served the
1539
1542
  // request. Don't penalize account health; record success to clear any
1540
1543
  // prior soft-avoid so a healthy account isn't stuck avoided.
@@ -1559,7 +1562,7 @@ export function codexForwardTerminalOutcomeRecorder(
1559
1562
  // the parent's terminalHttpStatus so the semantic status is not lost.
1560
1563
  const outcome = status === "completed"
1561
1564
  ? 200
1562
- : (httpStatusOverride ?? logCtx?.terminalHttpStatus ?? 502);
1565
+ : (quotaStatus ?? httpStatusOverride ?? logCtx?.terminalHttpStatus ?? 502);
1563
1566
  recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
1564
1567
  threadId: authCtx.affinityKey,
1565
1568
  fixedAccount: authCtx.fixedAccount,
@@ -1931,13 +1934,14 @@ export const UPSTREAM_JSON_BODY_READ_OPTIONS = {
1931
1934
  firstByteTimeoutMs: UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS,
1932
1935
  };
1933
1936
 
1934
- function unreadableEncryptedAgentTaskResponse(): Response {
1937
+ function unreadableEncryptedAgentTaskResponse(reason?: AgentTaskRecoveryFailureReason): Response {
1935
1938
  return new Response(
1936
1939
  JSON.stringify({
1937
1940
  error: {
1938
1941
  message: UNREADABLE_ENCRYPTED_AGENT_TASK_MESSAGE,
1939
1942
  type: "invalid_request_error",
1940
1943
  code: "unreadable_encrypted_agent_task",
1944
+ ...(reason === undefined ? {} : { recovery_reason: reason }),
1941
1945
  },
1942
1946
  }),
1943
1947
  { status: 400, headers: { "Content-Type": "application/json" } },
@@ -2530,6 +2534,7 @@ export async function handleComboResponses(
2530
2534
  const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
2531
2535
  comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
2532
2536
  let encryptedTaskRecoveryAttempted = false;
2537
+ let recoveryFailureReason: AgentTaskRecoveryFailureReason | undefined;
2533
2538
  let storedPool401ReplayDispatched = false;
2534
2539
  const recoverUnreadableEncryptedTask = async (): Promise<boolean> => {
2535
2540
  if (encryptedTaskRecoveryAttempted) return false;
@@ -2551,15 +2556,18 @@ export async function handleComboResponses(
2551
2556
  }
2552
2557
  let recovered = false;
2553
2558
  try {
2554
- recovered = await recoverEncryptedAgentTask(
2559
+ const result = await recoverEncryptedAgentTaskWithResult(
2555
2560
  req,
2556
2561
  (body as { input?: unknown } | undefined)?.input,
2557
2562
  recovery,
2558
2563
  config,
2559
2564
  { parentThreadId: inboundClientThreadId, abortSignal: options.abortSignal },
2560
2565
  );
2566
+ recovered = result.recovered;
2567
+ recoveryFailureReason = result.recovered ? undefined : result.reason;
2561
2568
  } catch {
2562
2569
  recovered = false;
2570
+ recoveryFailureReason = undefined;
2563
2571
  }
2564
2572
  // Recovery has the same in-place input mutation contract as the direct routed path.
2565
2573
  if (
@@ -2609,7 +2617,7 @@ export async function handleComboResponses(
2609
2617
  if (!(await recoverUnreadableEncryptedTask())) {
2610
2618
  return options.abortSignal?.aborted
2611
2619
  ? clientCancelledResponse()
2612
- : unreadableEncryptedAgentTaskResponse();
2620
+ : unreadableEncryptedAgentTaskResponse(recoveryFailureReason);
2613
2621
  }
2614
2622
  }
2615
2623
 
@@ -2668,7 +2676,20 @@ export async function handleComboResponses(
2668
2676
  attemptRetained = true;
2669
2677
  };
2670
2678
  let consumedChildFailure: ConsumedComboFailure | undefined;
2671
- const callbackGate = createChildPassthroughCallbackGate(options);
2679
+ const callbackGate = createChildPassthroughCallbackGate({
2680
+ ...options,
2681
+ onNativePassthroughTerminal: status => {
2682
+ // A committed stream can acquire terminal metadata after preflight copied
2683
+ // the child log. Publish it before the outer logger finalizes, but only
2684
+ // through the gate: discarded attempts must never affect the parent.
2685
+ // Undefined child fields must preserve metadata already inspected by WS.
2686
+ if (childLog.terminalHttpStatus !== undefined) logCtx.terminalHttpStatus = childLog.terminalHttpStatus;
2687
+ if (childLog.terminalIncompleteReason !== undefined) logCtx.terminalIncompleteReason = childLog.terminalIncompleteReason;
2688
+ if (childLog.terminalErrorCode !== undefined) logCtx.terminalErrorCode = childLog.terminalErrorCode;
2689
+ if (childLog.upstreamError !== undefined) logCtx.upstreamError = childLog.upstreamError;
2690
+ options.onNativePassthroughTerminal?.(status);
2691
+ },
2692
+ });
2672
2693
  let response: Response;
2673
2694
  try {
2674
2695
  const currentTargetProvider = pick.target.provider;
@@ -3400,6 +3421,7 @@ async function handleResponsesInner(
3400
3421
  previewSelectionAdmission?.release();
3401
3422
  }
3402
3423
 
3424
+ let recoveryFailureReason: AgentTaskRecoveryFailureReason | undefined;
3403
3425
  // Native fallback and explicitly trusted direct Responses routes can consume ciphertext,
3404
3426
  // so recover only after final route selection.
3405
3427
  if (
@@ -3418,15 +3440,18 @@ async function handleResponsesInner(
3418
3440
  (body as { input?: unknown } | undefined)?.input,
3419
3441
  );
3420
3442
  if (unreadableEncryptedAgentTask) try {
3421
- recovered = await recoverEncryptedAgentTask(
3443
+ const result = await recoverEncryptedAgentTaskWithResult(
3422
3444
  req,
3423
3445
  (body as { input?: unknown } | undefined)?.input,
3424
3446
  agentTaskRecovery,
3425
3447
  config,
3426
3448
  { parentThreadId, abortSignal: options.abortSignal },
3427
3449
  );
3450
+ recovered = result.recovered;
3451
+ recoveryFailureReason = result.recovered ? undefined : result.reason;
3428
3452
  } catch {
3429
3453
  recovered = false;
3454
+ recoveryFailureReason = undefined;
3430
3455
  }
3431
3456
  if (recovered) {
3432
3457
  unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
@@ -3550,7 +3575,7 @@ async function handleResponsesInner(
3550
3575
  && !finalRouteCanPassThroughEncryptedTask
3551
3576
  && unreadableEncryptedAgentTask
3552
3577
  ) {
3553
- return unreadableEncryptedAgentTaskResponse();
3578
+ return unreadableEncryptedAgentTaskResponse(recoveryFailureReason);
3554
3579
  }
3555
3580
 
3556
3581
  // The canonical ChatGPT backend rejects previous_response_id, so a local replay miss leaves no
@@ -5359,12 +5384,9 @@ async function handleResponsesInner(
5359
5384
  if (terminalBodyWillRecord) {
5360
5385
  options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
5361
5386
  terminalRecorder(status, httpStatusOverride);
5362
- if (status === "failed") {
5363
- const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
5364
- || logCtx.terminalHttpStatus === 429
5365
- || logCtx.terminalHttpStatus === 402
5366
- ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
5367
- : undefined;
5387
+ if (status === "failed" || status === "incomplete") {
5388
+ const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
5389
+ .find(value => value === 429 || value === 402);
5368
5390
  if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
5369
5391
  recordSubagentQuotaFailureForThreadSpawn(
5370
5392
  req.headers,
@@ -5570,12 +5592,9 @@ async function handleResponsesInner(
5570
5592
  const reportNativeTerminal = recordTerminalOutcomes
5571
5593
  ? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
5572
5594
  terminalRecorder?.(status, httpStatusOverride);
5573
- if (status === "failed") {
5574
- const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
5575
- || logCtx.terminalHttpStatus === 429
5576
- || logCtx.terminalHttpStatus === 402
5577
- ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
5578
- : undefined;
5595
+ if (status === "failed" || status === "incomplete") {
5596
+ const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
5597
+ .find(value => value === 429 || value === 402);
5579
5598
  if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
5580
5599
  recordSubagentQuotaFailureForThreadSpawn(
5581
5600
  req.headers,
@@ -5663,12 +5682,9 @@ async function handleResponsesInner(
5663
5682
  // client-cancel (no terminal seen) is finalized separately via consumeForInspection's onCancel.
5664
5683
  const reportNativeTerminal = (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
5665
5684
  terminalRecorder?.(status, httpStatusOverride);
5666
- if (status === "failed") {
5667
- const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
5668
- || logCtx.terminalHttpStatus === 429
5669
- || logCtx.terminalHttpStatus === 402
5670
- ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
5671
- : undefined;
5685
+ if (status === "failed" || status === "incomplete") {
5686
+ const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
5687
+ .find(value => value === 429 || value === 402);
5672
5688
  if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
5673
5689
  recordSubagentQuotaFailureForThreadSpawn(
5674
5690
  req.headers,
@@ -30,11 +30,11 @@ import {
30
30
  writeSync,
31
31
  chmodSync,
32
32
  } from "node:fs";
33
- import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
33
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
34
34
  import { Database } from "bun:sqlite";
35
35
  import { resolveCodexHomeDir } from "../codex/home";
36
36
  import { readThreadFieldsFromRollout } from "../codex/history-provider";
37
- import { renameAtomicFile } from "../config";
37
+ import { renameAtomicFile } from "../lib/windows-atomic-replace";
38
38
 
39
39
  export const ARCHIVED_SESSIONS_DIR = "archived_sessions";
40
40
  export const TRASH_DIR = ".trash";
@@ -115,9 +115,35 @@ function chmodPrivatePath(path: string, mode: number): void {
115
115
  try { chmodSync(path, mode); } catch { /* best-effort (e.g. Windows ACLs) */ }
116
116
  }
117
117
 
118
- function writePrivateFile(path: string, content: string): void {
119
- writeFileSync(path, content, "utf8");
120
- chmodPrivatePath(path, 0o600);
118
+ /** Publish complete stage metadata without truncating the last recovery record. */
119
+ function writePrivateFile(
120
+ path: string,
121
+ content: string,
122
+ beforeRename?: (temporaryPath: string, targetPath: string) => void,
123
+ ): void {
124
+ const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
125
+ let descriptor: number | undefined;
126
+ let created = false;
127
+ try {
128
+ descriptor = openSync(temporaryPath, "wx", 0o600);
129
+ created = true;
130
+ writeFileSync(descriptor, content, "utf8");
131
+ fsyncSync(descriptor);
132
+ closeSync(descriptor);
133
+ descriptor = undefined;
134
+ chmodPrivatePath(temporaryPath, 0o600);
135
+ beforeRename?.(temporaryPath, path);
136
+ renameAtomicFile(temporaryPath, path, undefined, "storage-cleanup");
137
+ chmodPrivatePath(path, 0o600);
138
+ fsyncDirectoryBestEffort(dirname(path));
139
+ } finally {
140
+ if (descriptor !== undefined) {
141
+ try { closeSync(descriptor); } catch { /* preserve publication failure */ }
142
+ }
143
+ if (created) {
144
+ try { unlinkSync(temporaryPath); } catch { /* renamed or cleanup unavailable */ }
145
+ }
146
+ }
121
147
  }
122
148
 
123
149
  function chunkIds(ids: string[], chunkSize: number): string[][] {
@@ -812,7 +838,6 @@ interface ReconcileTestHooks {
812
838
  const SATELLITE_BACKUP_FILE = "satellite-backup.json";
813
839
  /** Marks an incomplete restore so retries can accept dest files and resume metadata. */
814
840
  const RESTORE_PENDING_FILE = "restore-pending.json";
815
- let _satelliteBackupSeq = 0;
816
841
 
817
842
  type StagedFile = { from: string; to: string; relPath: string };
818
843
 
@@ -1070,34 +1095,11 @@ function writeSatelliteBackup(
1070
1095
  if (options?.failWrite) throw new Error("test_fail_satellite_backup_write");
1071
1096
  const dest = join(stageDir, SATELLITE_BACKUP_FILE);
1072
1097
  const replacing = existsSync(dest);
1073
- const tmp = join(stageDir, `${SATELLITE_BACKUP_FILE}.${process.pid}.${++_satelliteBackupSeq}.tmp`);
1074
- const payload = Buffer.from(JSON.stringify(backup), "utf8");
1075
- const fd = openSync(tmp, "w", 0o600);
1076
- try {
1077
- let offset = 0;
1078
- while (offset < payload.length) {
1079
- offset += writeSync(fd, payload, offset, payload.length - offset, null);
1098
+ writePrivateFile(dest, JSON.stringify(backup), () => {
1099
+ if (options?.failReplaceBeforeRename && replacing) {
1100
+ throw new Error("test_fail_satellite_backup_replace");
1080
1101
  }
1081
- fsyncSync(fd);
1082
- } catch (error) {
1083
- try { closeSync(fd); } catch { /* */ }
1084
- try { unlinkSync(tmp); } catch { /* */ }
1085
- throw error;
1086
- }
1087
- closeSync(fd);
1088
- chmodPrivatePath(tmp, 0o600);
1089
- if (options?.failReplaceBeforeRename && replacing) {
1090
- try { unlinkSync(tmp); } catch { /* */ }
1091
- throw new Error("test_fail_satellite_backup_replace");
1092
- }
1093
- try {
1094
- renameAtomicFile(tmp, dest, undefined, "storage-cleanup");
1095
- } catch (error) {
1096
- try { unlinkSync(tmp); } catch { /* */ }
1097
- throw error;
1098
- }
1099
- chmodPrivatePath(dest, 0o600);
1100
- fsyncDirectoryBestEffort(stageDir);
1102
+ });
1101
1103
  }
1102
1104
 
1103
1105
  function clearSatelliteBackup(stageDir: string): void {
@@ -1734,6 +1736,12 @@ export interface ExecuteCleanupOptions {
1734
1736
  /** Test-only failure injection for atomicity regressions. */
1735
1737
  _test?: {
1736
1738
  failManifestWrite?: boolean;
1739
+ /** Observe the complete temp and prior destination before publication. Never serialized. */
1740
+ beforeManifestReplace?: (
1741
+ temporaryPath: string,
1742
+ targetPath: string,
1743
+ phase: "staging" | "pre-commit" | "purge-incomplete",
1744
+ ) => void;
1737
1745
  failPurgeBasenames?: string[];
1738
1746
  failRollbackBasenames?: string[];
1739
1747
  blockStageDestBasenames?: string[];
@@ -1752,14 +1760,14 @@ export interface ExecuteCleanupOptions {
1752
1760
  /** Serializable cleanup test hooks allowed on the management API wire. */
1753
1761
  export type CleanupWireTestHooks = Omit<
1754
1762
  NonNullable<ExecuteCleanupOptions["_test"]>,
1755
- "afterSatelliteMutations" | "beforeReconcileLock"
1763
+ "afterSatelliteMutations" | "beforeReconcileLock" | "beforeManifestReplace"
1756
1764
  >;
1757
1765
 
1758
1766
  function isStringArray(v: unknown): v is string[] {
1759
1767
  return Array.isArray(v) && v.every(e => typeof e === "string");
1760
1768
  }
1761
1769
 
1762
- /** Pick only allowlisted serializable hooks; drops function hooks (afterSatelliteMutations, beforeReconcileLock) and unknown keys. */
1770
+ /** Pick only allowlisted serializable hooks; drops all function hooks and unknown keys. */
1763
1771
  export function pickWireCleanupTestHooks(raw: unknown): CleanupWireTestHooks | undefined {
1764
1772
  if (!raw || typeof raw !== "object") return undefined;
1765
1773
  const o = raw as Record<string, unknown>;
@@ -1911,6 +1919,9 @@ export function executeArchivedCleanup(options: ExecuteCleanupOptions): CleanupR
1911
1919
  entries: manifestEntries,
1912
1920
  ...extra,
1913
1921
  }, null, 2),
1922
+ (temporaryPath, targetPath) => options._test?.beforeManifestReplace?.(
1923
+ temporaryPath, targetPath, extra.staging ? "staging" : "pre-commit",
1924
+ ),
1914
1925
  );
1915
1926
  };
1916
1927
 
@@ -1999,6 +2010,9 @@ export function executeArchivedCleanup(options: ExecuteCleanupOptions): CleanupR
1999
2010
  }))
2000
2011
  .filter(entry => entry.physicalRelPaths.length > 0),
2001
2012
  }, null, 2),
2013
+ (temporaryPath, targetPath) => options._test?.beforeManifestReplace?.(
2014
+ temporaryPath, targetPath, "purge-incomplete",
2015
+ ),
2002
2016
  );
2003
2017
  } catch { /* best-effort: the pre-commit manifest is still on disk */ }
2004
2018
  return {
@@ -6,6 +6,8 @@ import type { CodexAccount } from "./accounts";
6
6
  * /v1/messages surface, the `ocx claude` launcher, and the GUI Claude page.
7
7
  */
8
8
  export interface OcxClaudeCodeConfig {
9
+ /** Opt-in translated Messages admission; unset keeps legacy behavior. Native passthrough is exempt. */
10
+ compatibility?: "shadow" | "enforce";
9
11
  /** Kill switch for the /v1/messages inbound (GUI "Claude ON" toggle). Default: enabled. */
10
12
  enabled?: boolean;
11
13
  /**
@@ -437,6 +439,8 @@ export interface OcxConfig {
437
439
  * Unset or empty leaves catalog priorities unchanged.
438
440
  */
439
441
  modelPickerOrder?: string[];
442
+ /** Saved preset provenance; snapshots are not recomputed during catalog discovery. */
443
+ modelPickerOrderMode?: "alphabetical" | "provider" | "most-used";
440
444
  /**
441
445
  * Priority-ordered fallback models for spawned sub-agents. When the requested
442
446
  * model is quota-exhausted or recently failed, opencodex rewrites the child
package/src/usage/log.ts CHANGED
@@ -9,6 +9,24 @@ import { usageDisplayTotalTokens } from "./totals";
9
9
  import type { AttemptTierOutcome, OcxUsage } from "../types";
10
10
  import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routing/trace";
11
11
  import { ACCOUNT_LOG_LABEL_RE, CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
12
+ import { claudeCompatibilityReason, normalizeClaudeFeatureCodes, type ClaudeFeatureCode } from "../claude/compatibility";
13
+
14
+ export interface PersistedClaudeCompatibilityLog {
15
+ decision: "shadow";
16
+ featureCodes: ClaudeFeatureCode[];
17
+ reason?: string;
18
+ }
19
+
20
+ /** Disk and in-memory callers share a closed-code projection; free-form reasons are discarded. */
21
+ export function normalizeClaudeCompatibilityUsageLog(value: unknown): PersistedClaudeCompatibilityLog | undefined {
22
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
23
+ const row = value as Record<string, unknown>;
24
+ if (row.decision !== "shadow") return undefined;
25
+ const featureCodes = normalizeClaudeFeatureCodes(row.featureCodes);
26
+ const reason = claudeCompatibilityReason(featureCodes, true);
27
+ if (!reason) return undefined;
28
+ return { decision: "shadow", featureCodes, reason };
29
+ }
12
30
 
13
31
  export type UsageStatus = "reported" | "unreported" | "unsupported" | "estimated";
14
32
  /**
@@ -160,6 +178,8 @@ export interface PersistedUsageEntry {
160
178
  * contains prompts, credentials, or hidden reasoning.
161
179
  */
162
180
  routeDecision?: RouteDecisionTraceV1;
181
+ /** Closed Claude protocol codes only; absent on older rows. */
182
+ claudeCompatibility?: PersistedClaudeCompatibilityLog;
163
183
  }
164
184
 
165
185
  const KNOWN_USAGE_SURFACES = new Set<NonNullable<PersistedUsageEntry["surface"]>>([
@@ -490,6 +510,7 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
490
510
  const callerServiceTier = sanitizeLogMetadataString(entry.callerServiceTier);
491
511
  const responseServiceTier = sanitizeLogMetadataString(entry.responseServiceTier);
492
512
  const shadowCallRewrittenFrom = sanitizeLogMetadataString(entry.shadowCallRewrittenFrom);
513
+ const claudeCompatibility = normalizeClaudeCompatibilityUsageLog(entry.claudeCompatibility);
493
514
  const routeDecision = entry.routeDecision
494
515
  ? normalizeRouteDecisionTrace(entry.routeDecision)
495
516
  : undefined;
@@ -563,6 +584,7 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
563
584
  ...(entry.closeReason ? { closeReason: entry.closeReason } : {}),
564
585
  ...(entry.upstreamError ? { upstreamError: entry.upstreamError } : {}),
565
586
  ...(routeDecision ? { routeDecision } : {}),
587
+ ...(claudeCompatibility ? { claudeCompatibility } : {}),
566
588
  };
567
589
  }
568
590