@bitkyc08/opencodex 2.40.0 → 2.42.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 (97) hide show
  1. package/README.md +4 -0
  2. package/gui/dist/assets/index-BU1tE0sr.js +112 -0
  3. package/gui/dist/assets/index-DL9-iS6J.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/meta.svg +1 -0
  6. package/package.json +4 -3
  7. package/src/adapters/cursor/catalog.ts +71 -29
  8. package/src/adapters/cursor/claude-id.ts +76 -0
  9. package/src/adapters/cursor/discovery.ts +16 -3
  10. package/src/adapters/cursor/effort-map.ts +27 -12
  11. package/src/adapters/cursor/protobuf-request.ts +41 -21
  12. package/src/adapters/google.ts +39 -2
  13. package/src/adapters/identity.ts +8 -2
  14. package/src/adapters/openai-responses.ts +57 -4
  15. package/src/bridge.ts +25 -3
  16. package/src/cli/account-auth.ts +28 -3
  17. package/src/cli/account-extended.ts +7 -1
  18. package/src/cli/capabilities.ts +2 -2
  19. package/src/cli/claude.ts +11 -2
  20. package/src/cli/connect.ts +7 -1
  21. package/src/cli/observe.ts +3 -1
  22. package/src/cli/registry.ts +1 -1
  23. package/src/cli/status.ts +19 -4
  24. package/src/client/connect.ts +5 -1
  25. package/src/client/hub-client.ts +29 -5
  26. package/src/clients/config-export.ts +12 -2
  27. package/src/codex/auth-api.ts +102 -9
  28. package/src/codex/catalog/aggregation.ts +8 -0
  29. package/src/codex/catalog/effort.ts +15 -2
  30. package/src/codex/catalog/metadata.ts +119 -9
  31. package/src/codex/catalog/native-models.ts +71 -0
  32. package/src/codex/catalog/parsing.ts +5 -3
  33. package/src/codex/catalog/provider-fetch.ts +166 -28
  34. package/src/codex/catalog.ts +1 -1
  35. package/src/codex/convergence-types.ts +1 -0
  36. package/src/codex/data/upstream-models.json +169 -0
  37. package/src/codex/desired-state.ts +18 -11
  38. package/src/codex/inject.ts +96 -6
  39. package/src/codex/injected-marker.ts +30 -4
  40. package/src/codex/journal.ts +14 -0
  41. package/src/combos/failover.ts +185 -6
  42. package/src/combos/index.ts +6 -0
  43. package/src/combos/resolve.ts +43 -6
  44. package/src/config.ts +5 -1
  45. package/src/generated/compatibility-version.json +115 -83
  46. package/src/generated/model-metadata.ts +1 -1
  47. package/src/grok/sync.ts +10 -2
  48. package/src/integrations/cursor-effort-table.ts +143 -0
  49. package/src/integrations/state.ts +1 -1
  50. package/src/integrations/writer.ts +2 -2
  51. package/src/lib/app-owned-memory-stores.ts +27 -8
  52. package/src/lib/bounded-body.ts +16 -1
  53. package/src/oauth/account-quota-rank.ts +40 -1
  54. package/src/oauth/chatgpt-device.ts +187 -0
  55. package/src/oauth/chatgpt.ts +31 -4
  56. package/src/oauth/generic-account-failover.ts +2 -2
  57. package/src/oauth/index.ts +24 -3
  58. package/src/oauth/log.ts +3 -0
  59. package/src/oauth/meta-muse.ts +235 -0
  60. package/src/providers/antigravity-models.ts +71 -13
  61. package/src/providers/command-code-efforts.ts +15 -0
  62. package/src/providers/free-directory.ts +4 -1
  63. package/src/providers/muse-subscription-usage.ts +95 -0
  64. package/src/providers/quota.ts +96 -0
  65. package/src/providers/registry.ts +116 -8
  66. package/src/responses/code-mode-helper-compat.ts +4 -1
  67. package/src/responses/state.ts +5 -4
  68. package/src/server/auth-cors.ts +241 -56
  69. package/src/server/chat-completions.ts +11 -2
  70. package/src/server/chat-native.ts +30 -4
  71. package/src/server/claude-messages.ts +17 -3
  72. package/src/server/effort-row.ts +131 -0
  73. package/src/server/index.ts +82 -45
  74. package/src/server/live.ts +18 -4
  75. package/src/server/management/api-key-rotation.ts +2 -1
  76. package/src/server/management/api-key-usage.ts +97 -43
  77. package/src/server/management/context.ts +3 -0
  78. package/src/server/management/cursor-integration-routes.ts +36 -7
  79. package/src/server/management/logs-usage-routes.ts +64 -87
  80. package/src/server/management/oauth-account-routes.ts +10 -3
  81. package/src/server/management/provider-routes.ts +218 -1
  82. package/src/server/management/route-registry.ts +1 -0
  83. package/src/server/management/usage-aggregate-cache.ts +464 -0
  84. package/src/server/management/usage-summary-cache.ts +4 -0
  85. package/src/server/models-capabilities.ts +60 -5
  86. package/src/server/responses/core.ts +95 -7
  87. package/src/server/responses/empty-completion-guard.ts +4 -0
  88. package/src/types/config.ts +10 -1
  89. package/src/types/request.ts +8 -0
  90. package/src/types/tools.ts +12 -9
  91. package/src/usage/expected-prices.ts +43 -7
  92. package/src/usage/ledger-scanner.ts +448 -0
  93. package/src/usage/log.ts +1 -1
  94. package/src/usage/summary.ts +915 -655
  95. package/src/web-search/index.ts +1 -1
  96. package/gui/dist/assets/index-BHe2rl_C.js +0 -112
  97. package/gui/dist/assets/index-CJSb3HPe.css +0 -1
@@ -68,12 +68,14 @@ import { resolvePassiveRouteSubjectId } from "../passive-route-linker";
68
68
  import {
69
69
  advanceComboAfterFailure,
70
70
  comboDefaultEffort,
71
+ comboFailureCooldownScope,
71
72
  comboFailureDecision,
72
73
  comboIdFromRawBody,
73
74
  comboRequestHasImageInput,
74
75
  concreteComboRequestBody,
75
76
  getCombo,
76
77
  isComboTargetInCooldown,
78
+ comboCooldownRetryAfterSeconds,
77
79
  NoAvailableComboTargetsError,
78
80
  noteComboSuccess,
79
81
  parseRetryAfterMs,
@@ -217,6 +219,9 @@ import {
217
219
  waitForProviderRequestSlot,
218
220
  } from "../../providers/request-pacing";
219
221
  import { slugsEquivalent } from "../../providers/slug-codec";
222
+ import { isMuseSubscriptionUsagePayload, parseMuseSubscriptionUsage } from "../../providers/muse-subscription-usage";
223
+ import { hasPassiveAccountQuota, recordPassiveAccountQuota } from "../../providers/quota";
224
+ import { captureConfigGeneration } from "../../lib/state-store-sweeper";
220
225
  import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
221
226
  import { isUsageDebugEnabled } from "../../usage/debug";
222
227
  import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "../request-decompress";
@@ -326,6 +331,7 @@ import {
326
331
  restoreImageGenCallsInJson,
327
332
  } from "../responses-image-gen-repair";
328
333
  import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from "../responses-model-rewrite";
334
+ import { parseRequestEffortRowId } from "../effort-row";
329
335
  import {
330
336
  collectSelfNamedNamespaceScrubAuthorization,
331
337
  createSelfNamedToolCallNamespaceScrubRewrite,
@@ -1434,15 +1440,29 @@ export function decodeRequestErrorResponse(err: unknown, label: string): Respons
1434
1440
 
1435
1441
 
1436
1442
 
1437
- export function comboUnavailableResponse(message: string): Response {
1443
+ export function comboUnavailableResponse(
1444
+ message: string,
1445
+ options?: { retryAfter?: string | null },
1446
+ ): Response {
1447
+ const headers = new Headers({ "Content-Type": "application/json" });
1448
+ const retryAfter = options?.retryAfter?.trim();
1449
+ if (retryAfter && retryAfter.length > 0 && retryAfter.length <= 128) {
1450
+ headers.set("Retry-After", retryAfter);
1451
+ }
1438
1452
  return new Response(
1439
1453
  JSON.stringify({
1440
1454
  error: { message, type: "server_error", code: "combo_unavailable" },
1441
1455
  }),
1442
- { status: 503, headers: { "Content-Type": "application/json" } },
1456
+ { status: 503, headers },
1443
1457
  );
1444
1458
  }
1445
1459
 
1460
+ function comboUnavailable(comboId: string, now = Date.now()): Response {
1461
+ return comboUnavailableResponse(`No available targets for combo: ${comboId}`, {
1462
+ retryAfter: comboCooldownRetryAfterSeconds(comboId, now),
1463
+ });
1464
+ }
1465
+
1446
1466
 
1447
1467
 
1448
1468
  export interface ConsumedComboFailure {
@@ -2297,7 +2317,7 @@ export async function handleComboResponses(
2297
2317
  config,
2298
2318
  { parentThreadId: inboundClientThreadId },
2299
2319
  );
2300
- return comboUnavailableResponse(`No available targets for combo: ${comboId}`);
2320
+ return comboUnavailable(comboId);
2301
2321
  }
2302
2322
  let recovered = false;
2303
2323
  try {
@@ -2334,7 +2354,7 @@ export async function handleComboResponses(
2334
2354
  }
2335
2355
 
2336
2356
  if (!pick) {
2337
- return comboUnavailableResponse(`No available targets for combo: ${comboId}`);
2357
+ return comboUnavailable(comboId);
2338
2358
  }
2339
2359
  // One immutable combo selection trace, before any child dispatch; child
2340
2360
  // adoption below must never replace it with a concrete child route trace.
@@ -2541,7 +2561,13 @@ export async function handleComboResponses(
2541
2561
  const nextPick = advanceComboAfterFailure(config, pick, {
2542
2562
  retryAfter: failure.retryAfter,
2543
2563
  now: Date.now(),
2564
+ cooldownScope: comboFailureCooldownScope(failure.response.status, failure.classificationText, {
2565
+ code: failure.upstreamCode,
2566
+ }),
2544
2567
  eligible: payloadEligible,
2568
+ status: failure.response.status,
2569
+ code: failure.upstreamCode,
2570
+ message: failure.classificationText,
2545
2571
  });
2546
2572
  if (!nextPick) adoptFailedChildLog(childLog);
2547
2573
  pick = nextPick;
@@ -2697,6 +2723,23 @@ async function handleResponsesInner(
2697
2723
  }
2698
2724
  return decodeRequestErrorResponse(err, "responses");
2699
2725
  }
2726
+ // An effort row naming a table-less combo (`combo/x--high`) must reach the combo dispatcher
2727
+ // as its base id, so the selector is normalized here, before comboIdFromRawBody reads model.
2728
+ const comboEffortRow = !options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)
2729
+ && typeof (body as { model?: unknown }).model === "string"
2730
+ ? parseRequestEffortRowId((body as { model: string }).model, config)
2731
+ : null;
2732
+ if (comboEffortRow) {
2733
+ const raw = body as Record<string, unknown>;
2734
+ raw.model = comboEffortRow.baseId;
2735
+ const rawReasoning = raw.reasoning;
2736
+ raw.reasoning = {
2737
+ ...(rawReasoning && typeof rawReasoning === "object" && !Array.isArray(rawReasoning)
2738
+ ? rawReasoning as Record<string, unknown>
2739
+ : {}),
2740
+ effort: comboEffortRow.effort,
2741
+ };
2742
+ }
2700
2743
  const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null;
2701
2744
  if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
2702
2745
  options.onRequestBodyRead?.();
@@ -2751,6 +2794,20 @@ async function handleResponsesInner(
2751
2794
  let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
2752
2795
  try {
2753
2796
  parsed = parseRequest(body);
2797
+ const effortRow = parseRequestEffortRowId(parsed.modelId, config);
2798
+ if (effortRow) {
2799
+ parsed.modelId = effortRow.baseId;
2800
+ parsed.options.reasoning = effortRow.effort;
2801
+ const raw = parsed._rawBody as Record<string, unknown>;
2802
+ const rawReasoning = raw.reasoning;
2803
+ raw.model = effortRow.baseId;
2804
+ raw.reasoning = {
2805
+ ...(rawReasoning && typeof rawReasoning === "object" && !Array.isArray(rawReasoning)
2806
+ ? rawReasoning as Record<string, unknown>
2807
+ : {}),
2808
+ effort: effortRow.effort,
2809
+ };
2810
+ }
2754
2811
  if (options.comboReplaySnapshot?.recoveredPlaintext) {
2755
2812
  markBodyNonPersistable(parsed._rawBody);
2756
2813
  }
@@ -2881,7 +2938,7 @@ async function handleResponsesInner(
2881
2938
  logCtx.routeDecision = route.routeDecision;
2882
2939
  } catch (err) {
2883
2940
  if (err instanceof NoAvailableComboTargetsError) {
2884
- return comboUnavailableResponse(err.message);
2941
+ return comboUnavailable(err.comboId);
2885
2942
  }
2886
2943
  if (err instanceof NoEligiblePolicyCandidateError) {
2887
2944
  // Persist the evaluation trace (per-candidate exclusions + the
@@ -2991,7 +3048,7 @@ async function handleResponsesInner(
2991
3048
  logCtx.routeDecision = route.routeDecision;
2992
3049
  } catch (err) {
2993
3050
  if (err instanceof NoAvailableComboTargetsError) {
2994
- return comboUnavailableResponse(err.message);
3051
+ return comboUnavailable(err.comboId);
2995
3052
  }
2996
3053
  if (err instanceof NoEligiblePolicyCandidateError) {
2997
3054
  logCtx.routeDecision = err.trace;
@@ -3115,7 +3172,7 @@ async function handleResponsesInner(
3115
3172
  logCtx.routeDecision = route.routeDecision;
3116
3173
  } catch (err) {
3117
3174
  if (err instanceof NoAvailableComboTargetsError) {
3118
- return comboUnavailableResponse(err.message);
3175
+ return comboUnavailable(err.comboId);
3119
3176
  }
3120
3177
  if (err instanceof NoEligiblePolicyCandidateError) {
3121
3178
  logCtx.routeDecision = err.trace;
@@ -3254,6 +3311,13 @@ async function handleResponsesInner(
3254
3311
  // the request actually used, so a concurrent rotation cannot cool an innocent replacement.
3255
3312
  let genericFailoverAccountId: string | null = null;
3256
3313
  let genericFailovers = 0;
3314
+ /**
3315
+ * Config generation captured where the serving credential is RESOLVED, not where the
3316
+ * quota is written. A streaming turn is a long await, so a generation captured at write
3317
+ * time cannot see a config or account change that happened earlier in the same turn —
3318
+ * the case the fence exists for. Stays 0 for every provider without a passive quota.
3319
+ */
3320
+ let passiveQuotaWriterGeneration = 0;
3257
3321
  /**
3258
3322
  * Apply a rotated account's FULL credential snapshot to the live route (#2568d).
3259
3323
  *
@@ -3389,6 +3453,10 @@ async function handleResponsesInner(
3389
3453
  if (isGenericFailoverProvider(route.providerName, route.provider)) {
3390
3454
  genericFailoverAccountId = resolved.accountId;
3391
3455
  }
3456
+ // Captured beside the account it fences, so the two can never disagree.
3457
+ if (hasPassiveAccountQuota(route.providerName)) {
3458
+ passiveQuotaWriterGeneration = captureConfigGeneration();
3459
+ }
3392
3460
  if (route.providerName === "kiro") {
3393
3461
  // `{}` is intentional: this is an account-scoped request with no stored routing metadata.
3394
3462
  // Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
@@ -3834,7 +3902,27 @@ async function handleResponsesInner(
3834
3902
  // check sees nothing undeclared, and the refused turn enters continuation state anyway. So the
3835
3903
  // rejection is sticky for the whole turn, set from every parsed payload on the inspection side.
3836
3904
  let inspectionSawUndeclaredTool = false;
3905
+ const passiveQuotaObserved = hasPassiveAccountQuota(route.providerName)
3906
+ && route.provider.authMode === "oauth";
3837
3907
  const noteInspectedPayload = (payload: unknown) => {
3908
+ // Meta reports subscription usage ONLY as an in-stream event; there is no endpoint
3909
+ // to poll (003 §E probed 17 paths, all 404). Observed here rather than behind a
3910
+ // dedicated inspector handler because onParsedPayload already reaches every
3911
+ // passthrough shape -- eager relay and both tee consumers -- through this one
3912
+ // function.
3913
+ //
3914
+ // Placed BEFORE the undeclared-tool early return below, which is load-bearing: that
3915
+ // guard latches for the rest of the turn once it fires, and a turn that tripped it
3916
+ // still legitimately reports usage.
3917
+ if (passiveQuotaObserved && isMuseSubscriptionUsagePayload(payload)) {
3918
+ const quota = parseMuseSubscriptionUsage(payload);
3919
+ // Read at EVENT time, not at handler construction: failover rebinds this, and the
3920
+ // quota belongs to the account that actually served the turn.
3921
+ const servingAccountId = genericFailoverAccountId;
3922
+ if (quota && servingAccountId) {
3923
+ recordPassiveAccountQuota(route.providerName, servingAccountId, quota, passiveQuotaWriterGeneration);
3924
+ }
3925
+ }
3838
3926
  // Gated on the same flag as the guard itself: with no readable catalog (or a forward-auth
3839
3927
  // provider) every name looks undeclared, and flipping this would stop recording continuation
3840
3928
  // state for exactly the passthrough traffic the guard deliberately stands down for.
@@ -176,6 +176,9 @@ export function mergeUsage(
176
176
  const contextTotalTokens = second.contextTotalTokens ?? first.contextTotalTokens;
177
177
  const inputTokens = first.inputTokens + second.inputTokens;
178
178
  const outputTokens = first.outputTokens + second.outputTokens;
179
+ // The attempt that produced the content owns the raw wire usage (openai/codex#41980);
180
+ // an empty first attempt may still be the only one that saw it.
181
+ const rawUsage = second.rawUsage ?? first.rawUsage;
179
182
  return {
180
183
  inputTokens,
181
184
  outputTokens,
@@ -186,6 +189,7 @@ export function mergeUsage(
186
189
  ...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
187
190
  ...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
188
191
  ...(first.estimated || second.estimated ? { estimated: true } : {}),
192
+ ...(rawUsage !== undefined ? { rawUsage } : {}),
189
193
  };
190
194
  }
191
195
 
@@ -348,7 +348,10 @@ export interface OcxConfig {
348
348
  * the guess is wrong.
349
349
  */
350
350
  oauthOpenBrowser?: boolean;
351
- /** Maximum usage-log bytes read for one management snapshot. */
351
+ /**
352
+ * @deprecated Compatibility-only limit for bounded legacy usage readers.
353
+ * `GET /api/usage` always aggregates the complete ledger.
354
+ */
352
355
  managementUsageMaxReadBytes?: number;
353
356
  providers: Record<string, OcxProviderConfig>;
354
357
  defaultProvider: string;
@@ -366,6 +369,12 @@ export interface OcxConfig {
366
369
  };
367
370
  /** Enable the shipped model alias patterns for providers without an override. */
368
371
  defaultModelAliases?: boolean;
372
+ /**
373
+ * Opt-in Cursor Private Inference compatibility rows. When true, `/v1/models`
374
+ * adds `<base-id>--<effort>` selectors for reasoning-capable model ids absent
375
+ * from Cursor's built-in effort table. Omitted/false preserves discovery output.
376
+ */
377
+ cursorEffortRows?: boolean;
369
378
  /** Explicit top-level deletion intent used by stale whole-config rebases. */
370
379
  configRebaseProvenance?: OcxConfigRebaseProvenance | Record<string, unknown>;
371
380
  /** OpenAI provider-contract migration marker (v2 = single `openai` provider with account mode). */
@@ -396,4 +396,12 @@ export interface OcxUsage {
396
396
  cacheCreationInputTokens?: number;
397
397
  reasoningOutputTokens?: number;
398
398
  estimated?: boolean;
399
+ /**
400
+ * The raw upstream usage object for Responses-shaped upstreams (openai/codex#41980 parity):
401
+ * codex-rs preserves the complete `response.usage` object through its own pipeline, so fields
402
+ * the proxy does not model (subscription metadata, future counters) must survive the bridged /
403
+ * rebuilt `response.completed` too. Accounting paths read only the canonical fields above; the
404
+ * wire rebuild merges this object's unknown keys back under the normalized values.
405
+ */
406
+ rawUsage?: Record<string, unknown>;
399
407
  }
@@ -35,20 +35,23 @@ export function namespacedToolName(namespace: string | undefined, name: string):
35
35
  * Codex unified-exec name normalization.
36
36
  *
37
37
  * Codex's code-mode shell tool is declared as `exec` (a freeform custom tool whose own
38
- * description mentions the nested `await tools.exec_command(...)` helper). Routed models
39
- * DeepSeek in particular — sometimes echo that helper name as the tool-call name, emitting
40
- * `exec_command` or `apply_patch` instead of the declared `exec`. Accept these nested helper
41
- * names only when the request catalog actually declares `exec` and does not itself declare the
42
- * emitted name (an MCP server may legitimately advertise one under its own namespace).
38
+ * description mentions the nested `await tools.exec_command(...)` helper). Some routed providers
39
+ * echo that helper name as the tool-call name, emitting `exec_command`, `write_stdin`, or
40
+ * `apply_patch` instead of the declared `exec`. Accept these nested helper names only when the
41
+ * request catalog actually declares `exec` and does not itself declare the emitted name (an MCP
42
+ * server may legitimately advertise one under its own namespace).
43
43
  */
44
44
  const LEGACY_SHELL_BRIDGE_TOOL_NAMES = ["exec_command", "shell_command"] as const;
45
- const CODE_MODE_HELPER_TOOL_NAMES = [...LEGACY_SHELL_BRIDGE_TOOL_NAMES, "apply_patch"] as const;
45
+ const CODE_MODE_HELPER_TOOL_NAMES = [
46
+ ...LEGACY_SHELL_BRIDGE_TOOL_NAMES,
47
+ "write_stdin",
48
+ "apply_patch",
49
+ ] as const;
46
50
 
47
51
  /**
48
52
  * The one declared name that turns nested-helper normalization on. Declaring it is not just a
49
- * name: it also decides whether an emitted `exec_command`/`shell_command`/`apply_patch` is
50
- * accepted as that shell tool, so callers that build declared-name sets must add it only for a
51
- * genuine bare declaration.
53
+ * name: it also decides whether an emitted helper name is accepted as that shell tool, so callers
54
+ * that build declared-name sets must add it only for a genuine bare declaration.
52
55
  */
53
56
  export const CODE_MODE_EXEC_TOOL_NAME = "exec";
54
57
 
@@ -1,3 +1,5 @@
1
+ import { normalizeCursorClaudeId } from "../adapters/cursor/claude-id";
2
+
1
3
  /**
2
4
  * Expected-price overlay for models whose jawcode cost rows are missing or all-zero
3
5
  * (subscription/OAuth surfaces). Sourced from official pricing pages only
@@ -56,6 +58,10 @@ const GEMINI_36_FLASH: Cost4 = { input: 1.5, output: 7.5, cacheRead: 0.15, cache
56
58
  // through 2026-12-31, stepping up to $1.50 / $7.50 on 2027-01-01. Revisit this row
57
59
  // then — the promotional rate is dated on the pricing page, not open-ended.
58
60
  const GEMINI_37_FLASH: Cost4 = { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0 };
61
+ // Gemini 3.8 Flash carries the same published promotional shape as 3.7 through 2026-12-31,
62
+ // rising to $1.50 / $7.50 on 2027-01-01. A SEPARATE constant on purpose: equal today, but
63
+ // aliasing them would silently drag 3.8 along if 3.7's row is ever re-verified differently.
64
+ const GEMINI_38_FLASH: Cost4 = { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0 };
59
65
  const MINIMAX_M21_HIGHSPEED: Cost4 = { input: 0.6, output: 2.4, cacheRead: 0.03, cacheWrite: 0.375 };
60
66
  const KIMI_K3: Cost4 = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3 };
61
67
  const KIMI_K27_CODE: Cost4 = { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0.95 };
@@ -79,8 +85,19 @@ const ANTHROPIC_PRICING = "https://platform.claude.com/docs/en/about-claude/pric
79
85
 
80
86
  const GEMINI_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-07-22); cacheWrite=0: storage is billed per-hour, not per-token";
81
87
  const GEMINI_37_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-08-14); promotional rate through 2026-12-31, rises to 1.50/7.50 on 2027-01-01; cacheWrite=0: storage is billed per-hour, not per-token";
88
+ const GEMINI_38_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-09-03); promotional rate through 2026-12-31, rises to 1.50/7.50 on 2027-01-01; cacheWrite=0: storage is billed per-hour, not per-token";
82
89
  const MINIMAX_PRICING = "https://platform.minimax.io/docs/guides/pricing-paygo";
83
90
  const OPENAI_GPT56_PRICING = "https://developers.openai.com/api/docs/pricing";
91
+ const META_MODEL_PRICING = "https://dev.meta.ai/docs/pricing-rate-limits";
92
+ /*
93
+ * Shared by both Meta providers. Overlays resolve by EXACT provider id, so `meta-muse`
94
+ * cannot inherit `meta-model`'s rows — and an unpriced provider whose whole warning is
95
+ * "treat every call as billable" would report no cost at all.
96
+ */
97
+ const META_MUSE_SPARK_13: Cost4 = { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 };
98
+ const META_MUSE_SPARK_13_CONTRIBUTOR: Cost4 = { input: 0.1, output: 0.2, cacheRead: 0.002, cacheWrite: 0 };
99
+ const META_SPARK_SOURCE = `Meta Model API published price ${META_MODEL_PRICING}`;
100
+ const META_SPARK_CONTRIBUTOR_SOURCE = `Meta Model API published Contributor-tier price ${META_MODEL_PRICING}; data-sharing discount tier`;
84
101
  const DEEPSEEK_PRICING = "https://api-docs.deepseek.com/quick_start/pricing-details-usd; V4 Flash alias transition scheduled 2026-07-24 — re-verify after";
85
102
  // Kimi official tables publish input/output/cache-hit only; cacheWrite is mapped to the
86
103
  // cache-miss input price (Kimi auto-caches with no separate write billing). 2026-07-20 re-verified.
@@ -100,13 +117,8 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
100
117
  // bundle collapses anthropic-apikey onto anthropic).
101
118
  { provider: "anthropic", modelId: "claude-fable-5-1", cost4: CLAUDE_FABLE_51, source: `anthropic official Claude Fable 5.1 ${ANTHROPIC_PRICING}; cache hit = 0.025x base input`, verifiedAt: "2026-09-02", status: "verified" },
102
119
  { provider: "anthropic-apikey", modelId: "claude-fable-5-1", cost4: CLAUDE_FABLE_51, source: `anthropic official Claude Fable 5.1 ${ANTHROPIC_PRICING}; cache hit = 0.025x base input`, verifiedAt: "2026-09-02", status: "verified" },
103
- // Cursor seeds Fable 5.1 preemptively under three spellings (adapters/cursor/catalog.ts);
104
- // the model-level vendor fallback only searches jawcode metadata, which has no Fable 5.1
105
- // row yet, so each Cursor spelling needs its own overlay. Vendor list price, like the
106
- // cursor/claude-opus-5 row.
120
+ // Cursor canonicalizes every Fable 5.1 spelling onto this sole overlay row.
107
121
  { provider: "cursor", modelId: "claude-fable-5-1", cost4: CLAUDE_FABLE_51, source: `anthropic official Claude Fable 5.1 ${ANTHROPIC_PRICING}; cache hit = 0.025x base input; vendor list price applied to the Cursor surface`, verifiedAt: "2026-09-02", status: "verified-derived" },
108
- { provider: "cursor", modelId: "claude-fable-5.1", cost4: CLAUDE_FABLE_51, source: `anthropic official Claude Fable 5.1 ${ANTHROPIC_PRICING}; cache hit = 0.025x base input; vendor list price applied to the Cursor surface`, verifiedAt: "2026-09-02", status: "verified-derived" },
109
- { provider: "cursor", modelId: "claude-5.1-fable", cost4: CLAUDE_FABLE_51, source: `anthropic official Claude Fable 5.1 ${ANTHROPIC_PRICING}; cache hit = 0.025x base input; vendor list price applied to the Cursor surface`, verifiedAt: "2026-09-02", status: "verified-derived" },
110
122
  // claude-opus-5 is exposed by three providers but absent from the jawcode bundle, so
111
123
  // cost resolution returned null and the Logs `~$` column rendered an em dash. The
112
124
  // model-level vendor fallback only searches jawcode metadata, never overlays, so one
@@ -126,6 +138,10 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
126
138
  // 3.7 Flash rides CCA, whose billing equivalence to the Developer API list price is
127
139
  // not published, so this is `verified-derived` rather than `verified`: the number is
128
140
  // proven, the claim that Antigravity charges it is inferred.
141
+ { provider: "google-antigravity", modelId: "gemini-3.8-flash", cost4: GEMINI_38_FLASH, source: `derived: Gemini 3.8 Flash promotional rate through 2026-12-31 ${GEMINI_38_PRICING}`, verifiedAt: "2026-09-03", status: "verified-derived" },
142
+ { provider: "google-antigravity", modelId: "gemini-3.8-flash-low", cost4: GEMINI_38_FLASH, source: `derived: gemini-3.8-flash ${GEMINI_38_PRICING}`, verifiedAt: "2026-09-03", status: "verified-derived" },
143
+ { provider: "google-antigravity", modelId: "gemini-3.8-flash-medium", cost4: GEMINI_38_FLASH, source: `derived: gemini-3.8-flash ${GEMINI_38_PRICING}`, verifiedAt: "2026-09-03", status: "verified-derived" },
144
+ { provider: "google-antigravity", modelId: "gemini-3.8-flash-high", cost4: GEMINI_38_FLASH, source: `derived: gemini-3.8-flash ${GEMINI_38_PRICING}`, verifiedAt: "2026-09-03", status: "verified-derived" },
129
145
  { provider: "google-antigravity", modelId: "gemini-3.7-flash", cost4: GEMINI_37_FLASH, source: `derived: Gemini 3.7 Flash promotional rate through 2026-12-31 ${GEMINI_37_PRICING}`, verifiedAt: "2026-08-14", status: "verified-derived" },
130
146
  // Retained after the 3.6 retirement: historical usage.jsonl rows still carry these
131
147
  // ids, and dropping the row would silently zero the cost of requests already made.
@@ -139,6 +155,19 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
139
155
  { provider: "openai-apikey", modelId: "gpt-5.6-sol-pro", cost4: GPT56_SOL, source: `collapsed base ID ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-03", status: "verified-derived" },
140
156
  { provider: "openai-apikey", modelId: "gpt-5.6-terra-pro", cost4: GPT56_TERRA, source: `collapsed base ID ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-03", status: "verified-derived" },
141
157
  { provider: "openai-apikey", modelId: "gpt-5.6-luna-pro", cost4: GPT56_LUNA, source: `collapsed base ID ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-03", status: "verified-derived" },
158
+ // Meta Model API direct provider. `meta-model` has no jawcode metadata alias, so an
159
+ // unpriced row falls through the whole resolution chain and the Logs cost column
160
+ // renders nothing — these exact overlays are the only source. Both are Meta's own
161
+ // published list prices for Meta's own endpoint (hence "verified", not derived), and
162
+ // they match the figures Command Code republishes for the same two models.
163
+ // cacheWrite=0: Meta publishes a cached-input price but no cache-write charge.
164
+ { provider: "meta-model", modelId: "muse-spark-1.3", cost4: META_MUSE_SPARK_13, source: META_SPARK_SOURCE, verifiedAt: "2026-09-03", status: "verified" },
165
+ { provider: "meta-model", modelId: "muse-spark-1.3-contributor", cost4: META_MUSE_SPARK_13_CONTRIBUTOR, source: META_SPARK_CONTRIBUTOR_SOURCE, verifiedAt: "2026-09-03", status: "verified" },
166
+ // Same endpoint, same list price, different credential. Meta does not authorize this
167
+ // reuse and settlement is not observable, so these are the public Model API rates as a
168
+ // conservative estimate — not evidence of how the call is actually billed.
169
+ { provider: "meta-muse", modelId: "muse-spark-1.3", cost4: META_MUSE_SPARK_13, source: META_SPARK_SOURCE, verifiedAt: "2026-09-03", status: "verified-derived" },
170
+ { provider: "meta-muse", modelId: "muse-spark-1.3-contributor", cost4: META_MUSE_SPARK_13_CONTRIBUTOR, source: META_SPARK_CONTRIBUTOR_SOURCE, verifiedAt: "2026-09-03", status: "verified-derived" },
142
171
  // Daybreak aliases: priced as their current snapshots (red -> gpt-5.6-cyber,
143
172
  // blue -> gpt-5.6-sol). The alias ids carry no rows of their own upstream, hence
144
173
  // verified-derived. Blue deliberately reuses GPT56_SOL rather than duplicating the tuple.
@@ -163,6 +192,7 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
163
192
  { provider: "google", modelId: "gemini-3.6-flash", cost4: GEMINI_36_FLASH, source: GEMINI_PRICING, verifiedAt: "2026-07-22", status: "verified" },
164
193
  // Developer API row: the price IS published for this surface, so `verified`.
165
194
  { provider: "google", modelId: "gemini-3.7-flash", cost4: GEMINI_37_FLASH, source: GEMINI_37_PRICING, verifiedAt: "2026-08-14", status: "verified" },
195
+ { provider: "google", modelId: "gemini-3.8-flash", cost4: GEMINI_38_FLASH, source: GEMINI_38_PRICING, verifiedAt: "2026-09-03", status: "verified" },
166
196
  { provider: "google-antigravity", modelId: "gemini-3.1-pro-preview", cost4: GEMINI_31_PRO, source: GEMINI_PRICING, verifiedAt: "2026-07-20", status: "verified" },
167
197
  // Antigravity-bundled third-party models — derived from the underlying vendor's
168
198
  // official API price (Antigravity itself bills via subscription quota).
@@ -236,8 +266,14 @@ export function findExpectedPriceOverlay(
236
266
  overlays: readonly ExpectedPriceOverlay[] = EXPECTED_PRICE_OVERLAYS,
237
267
  ): ExpectedPriceOverlay | undefined {
238
268
  const exact = overlays.filter(row => row.provider === provider && row.modelId === modelId);
239
- return exact.find(row => row.status === "verified")
269
+ const match = exact.find(row => row.status === "verified")
240
270
  ?? exact.find(row => row.status === "verified-derived");
271
+ if (match || provider !== "cursor") return match;
272
+ const canonicalBaseId = normalizeCursorClaudeId(modelId)?.canonicalBaseId;
273
+ if (!canonicalBaseId) return undefined;
274
+ const canonical = overlays.filter(row => row.provider === provider && row.modelId === canonicalBaseId);
275
+ return canonical.find(row => row.status === "verified")
276
+ ?? canonical.find(row => row.status === "verified-derived");
241
277
  }
242
278
 
243
279
  /** OpenAI Fast price multipliers retained as a compatibility export. */