@bitkyc08/opencodex 2.7.35 → 2.7.36

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 (80) hide show
  1. package/README.ja.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +4 -2
  4. package/README.ru.md +1 -1
  5. package/README.zh-CN.md +1 -1
  6. package/bin/ocx.mjs +52 -0
  7. package/gui/dist/assets/index-BpX-hoSd.css +1 -0
  8. package/gui/dist/assets/index-ZmFopEYw.js +52 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -1
  11. package/src/adapters/cursor/cursor-errors.ts +38 -1
  12. package/src/adapters/cursor/discovery.ts +1 -0
  13. package/src/adapters/cursor/effort-map.ts +1 -0
  14. package/src/adapters/cursor/live-models.ts +22 -5
  15. package/src/adapters/cursor/live-transport.ts +82 -7
  16. package/src/adapters/cursor/transport.ts +2 -0
  17. package/src/adapters/cursor.ts +5 -2
  18. package/src/adapters/openai-responses.ts +64 -1
  19. package/src/cli/doctor.ts +10 -0
  20. package/src/cli/help.ts +10 -0
  21. package/src/cli/index.ts +88 -9
  22. package/src/cli/internal-dispatch.ts +20 -0
  23. package/src/cli/status.ts +15 -4
  24. package/src/cli/tray-proxy.ts +52 -0
  25. package/src/codex/auth-api.ts +46 -5
  26. package/src/codex/autostart-health.ts +149 -0
  27. package/src/codex/catalog/aggregation.ts +268 -0
  28. package/src/codex/catalog/bundled.ts +188 -0
  29. package/src/codex/catalog/effort.ts +263 -0
  30. package/src/codex/catalog/metadata.ts +176 -0
  31. package/src/codex/catalog/parsing.ts +399 -0
  32. package/src/codex/catalog/provider-fetch.ts +609 -0
  33. package/src/codex/catalog/sync.ts +540 -0
  34. package/src/codex/catalog.ts +11 -2426
  35. package/src/codex/inject.ts +165 -3
  36. package/src/codex/shim.ts +141 -8
  37. package/src/codex/sync.ts +17 -2
  38. package/src/config.ts +23 -0
  39. package/src/lib/errors.ts +11 -0
  40. package/src/providers/antigravity-models.ts +33 -0
  41. package/src/providers/kiro-models.ts +2 -0
  42. package/src/providers/registry.ts +2 -2
  43. package/src/responses/state.ts +69 -6
  44. package/src/server/auth-cors.ts +3 -0
  45. package/src/server/management/agent-settings-routes.ts +536 -0
  46. package/src/server/management/combo-routes.ts +210 -0
  47. package/src/server/management/config-routes.ts +302 -0
  48. package/src/server/management/context.ts +21 -0
  49. package/src/server/management/logs-usage-routes.ts +176 -0
  50. package/src/server/management/model-routes.ts +253 -0
  51. package/src/server/management/oauth-account-routes.ts +301 -0
  52. package/src/server/management/provider-routes.ts +408 -0
  53. package/src/server/management/shared.ts +186 -0
  54. package/src/server/management-api.ts +23 -1806
  55. package/src/server/responses/collaboration.ts +300 -0
  56. package/src/server/responses/compact.ts +342 -0
  57. package/src/server/responses/core.ts +1498 -0
  58. package/src/server/responses/encrypted-payload.ts +231 -0
  59. package/src/server/responses/fetch-helpers.ts +157 -0
  60. package/src/server/responses.ts +9 -2172
  61. package/src/server/startup-action-control.ts +41 -0
  62. package/src/server/startup-health-cache.ts +100 -0
  63. package/src/server/windows-tray-control.ts +41 -0
  64. package/src/service.ts +171 -19
  65. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  66. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  67. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  68. package/src/tray/assets/opencodex-tray.png +0 -0
  69. package/src/tray/windows-tray.ps1 +290 -0
  70. package/src/tray/windows.ts +628 -0
  71. package/src/types.ts +5 -0
  72. package/src/update/index.ts +43 -0
  73. package/src/update/job.ts +46 -0
  74. package/src/update/tray-update-plan.d.mts +18 -0
  75. package/src/update/tray-update-plan.mjs +38 -0
  76. package/src/usage/cost.ts +0 -0
  77. package/src/usage/expected-prices.ts +9 -2
  78. package/src/usage/summary.ts +42 -7
  79. package/gui/dist/assets/index-BunUANVE.js +0 -52
  80. package/gui/dist/assets/index-Sg-7L_oZ.css +0 -1
@@ -3,6 +3,7 @@ import { readFileSync, readdirSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { dirname, join } from "node:path";
5
5
  import { getConfigDir, loadConfig, readPid, readRuntimePort } from "../config";
6
+ import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-update-plan.mjs";
6
7
 
7
8
  /**
8
9
  * A `codex-history-backup-*.json` surviving a stop means the native-history restore was
@@ -170,6 +171,26 @@ export async function runUpdate(): Promise<void> {
170
171
  const { isServiceInstalled } = await import("../service");
171
172
  serviceWasInstalled = isServiceInstalled();
172
173
  } catch { /* best-effort */ }
174
+ let trayWasInstalled = false;
175
+ let trayWasRunning = false;
176
+ if (process.platform === "win32") {
177
+ try {
178
+ const { getWindowsTrayStatus, startWindowsTray, stopWindowsTray } = await import("../tray/windows");
179
+ const tray = getWindowsTrayStatus();
180
+ const trayPlan = handoffWindowsTrayForUpdate(tray, {
181
+ stop: () => {
182
+ const stopped = stopWindowsTray();
183
+ return { exitStatus: 0, running: stopped.running };
184
+ },
185
+ start: () => startWindowsTray(),
186
+ });
187
+ trayWasInstalled = trayPlan.refreshAfterReplacement;
188
+ trayWasRunning = trayPlan.restoreOnFailure;
189
+ } catch (error) {
190
+ console.error(`⚠️ Could not stop the Windows tray; aborting before package replacement: ${error instanceof Error ? error.message : String(error)}`);
191
+ process.exit(1);
192
+ }
193
+ }
173
194
 
174
195
  // Capture listen target before stop clears runtime state (same contract as GUI update worker).
175
196
  // Prefer a live runtime record; a stale crashed leftover must not override config.port.
@@ -200,6 +221,12 @@ export async function runUpdate(): Promise<void> {
200
221
  });
201
222
  if (stopStdio === "pipe") logSpawnOutput("", stop);
202
223
  if (stop.status !== 0 || readPid() || readRuntimePort()) {
224
+ if (trayWasRunning) {
225
+ try {
226
+ const { startWindowsTray } = await import("../tray/windows");
227
+ startWindowsTray();
228
+ } catch { /* preserve the proxy stop failure */ }
229
+ }
203
230
  console.error("⚠️ Could not stop the running proxy; aborting the update. Run 'ocx stop' and retry.");
204
231
  process.exit(1);
205
232
  }
@@ -238,6 +265,16 @@ export async function runUpdate(): Promise<void> {
238
265
  } catch (e) {
239
266
  console.warn(`⚠️ Shim repair skipped: ${e instanceof Error ? e.message : e}`);
240
267
  }
268
+ if (trayWasInstalled) {
269
+ const trayArgs = [process.argv[1], ...planWindowsTrayUpdate({ installed: trayWasInstalled, running: trayWasRunning }).installArgs];
270
+ const tray = spawnSync(process.execPath, trayArgs, { stdio: "inherit", windowsHide: true });
271
+ if (tray.status === 0) {
272
+ console.log("🔧 Refreshed Windows tray startup paths.");
273
+ } else {
274
+ console.warn("⚠️ Windows tray refresh failed. Run 'ocx tray install'.");
275
+ if (trayWasRunning) spawnSync(process.execPath, [process.argv[1], "tray", "start"], { stdio: "ignore", windowsHide: true });
276
+ }
277
+ }
241
278
  // The stop above unloaded any managed service; reinstall it with the NEW files
242
279
  // (spawn the fresh cli.ts so updated code writes the baked paths) so a
243
280
  // launchd/schtasks/systemd user isn't left with the background proxy down.
@@ -286,6 +323,12 @@ export async function runUpdate(): Promise<void> {
286
323
  console.log("Restart the proxy: ocx start");
287
324
  }
288
325
  } else {
326
+ if (trayWasRunning) {
327
+ try {
328
+ const { startWindowsTray } = await import("../tray/windows");
329
+ startWindowsTray();
330
+ } catch { /* keep the primary update failure */ }
331
+ }
289
332
  console.error(`\n⚠️ Update failed (${bin} exit ${r.status ?? "?"}). Try manually: ${bin} ${cmdArgs.join(" ")}`);
290
333
  process.exit(1);
291
334
  }
package/src/update/job.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  updateCommandStr,
21
21
  } from "./index";
22
22
  import { isNewer } from "./notify";
23
+ import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-update-plan.mjs";
23
24
 
24
25
  const RELEASE_NOTES_URL = "https://github.com/lidge-jun/opencodex/releases/latest";
25
26
  const UPDATE_JOB_FILENAME = "update-job.json";
@@ -474,6 +475,8 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar
474
475
  port: runtimeTrusted ? rt.port : configPort,
475
476
  hostname: (runtimeTrusted ? rt.hostname : undefined) ?? preUpdateConfig.hostname ?? "127.0.0.1",
476
477
  };
478
+ let trayWasInstalled = false;
479
+ let trayWasRunning = false;
477
480
  if (!job) {
478
481
  job = {
479
482
  id: jobId,
@@ -517,6 +520,28 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar
517
520
  command: cmd.display,
518
521
  }, integrityLine);
519
522
 
523
+ if (process.platform === "win32") {
524
+ try {
525
+ const { getWindowsTrayStatus, startWindowsTray, stopWindowsTray } = await import("../tray/windows");
526
+ const tray = getWindowsTrayStatus();
527
+ const trayPlan = handoffWindowsTrayForUpdate(tray, {
528
+ stop: () => {
529
+ const stopped = stopWindowsTray();
530
+ return { exitStatus: 0, running: stopped.running };
531
+ },
532
+ start: () => startWindowsTray(),
533
+ });
534
+ trayWasInstalled = trayPlan.refreshAfterReplacement;
535
+ trayWasRunning = trayPlan.restoreOnFailure;
536
+ } catch (error) {
537
+ updateJob(job, {
538
+ status: "failed",
539
+ error: `Could not stop the Windows tray; aborting before package replacement: ${error instanceof Error ? error.message : String(error)}`,
540
+ });
541
+ return;
542
+ }
543
+ }
544
+
520
545
  /* [Decision Log]
521
546
  - 목적: GUI 요청 처리 프로세스가 자신이 실행 중인 패키지를 직접 덮어쓰지 않도록 업데이트를 별도 worker에서 수행한다.
522
547
  - 대안 분석: (1) 서버에서 runUpdate 직접 호출: process.exit/stdio/실행 파일 교체 위험. (2) GUI에서 CLI 명령 안내만 제공: 자동 업데이트 UX 부족. (3) 숨은 worker가 Node launcher/Bun 전역 명령을 실행: 상태 추적과 안전한 재시작이 가능.
@@ -524,6 +549,12 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar
524
549
  */
525
550
  const result = runLoggedCommand(job, cmd.bin, cmd.args, UPDATE_TIMEOUT_MS);
526
551
  if (result.status !== 0) {
552
+ if (trayWasRunning) {
553
+ try {
554
+ const { startWindowsTray } = await import("../tray/windows");
555
+ startWindowsTray();
556
+ } catch { /* retain the primary update failure */ }
557
+ }
527
558
  updateJob(job, {
528
559
  status: "failed",
529
560
  exitCode: result.status,
@@ -533,6 +564,15 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar
533
564
  return;
534
565
  }
535
566
 
567
+ if (trayWasInstalled) {
568
+ const trayArgs = [process.argv[1], ...planWindowsTrayUpdate({ installed: trayWasInstalled, running: trayWasRunning }).installArgs];
569
+ const tray = runLoggedCommand(job, process.execPath, trayArgs, 20_000);
570
+ if (tray.status !== 0) {
571
+ updateJob(job, {}, "Windows tray refresh failed; run 'ocx tray install'.");
572
+ if (trayWasRunning) runLoggedCommand(job, process.execPath, [process.argv[1], "tray", "start"], 15_000);
573
+ }
574
+ }
575
+
536
576
  if (restart) {
537
577
  job = updateJob(job, { status: "restarting" }, "Update installed. Restarting proxy...");
538
578
  await restartAfterUpdate(job, captured);
@@ -543,6 +583,12 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar
543
583
 
544
584
  updateJob(job, { status: "succeeded", restarted: false }, "Update installed. Restart the proxy to use the new version.");
545
585
  } catch (err) {
586
+ if (trayWasRunning) {
587
+ try {
588
+ const { startWindowsTray } = await import("../tray/windows");
589
+ startWindowsTray();
590
+ } catch { /* retain the primary worker failure */ }
591
+ }
546
592
  updateJob(job, {
547
593
  status: "failed",
548
594
  error: err instanceof Error ? err.message : String(err),
@@ -0,0 +1,18 @@
1
+ export interface WindowsTrayUpdatePlan {
2
+ installed: boolean;
3
+ running: boolean;
4
+ stopBeforeReplacement: boolean;
5
+ restoreOnFailure: boolean;
6
+ refreshAfterReplacement: boolean;
7
+ installArgs: string[];
8
+ }
9
+
10
+ export declare function planWindowsTrayUpdate(status: { installed?: boolean; running?: boolean }): WindowsTrayUpdatePlan;
11
+ export declare function windowsTrayStopConfirmed(exitStatus: number | null, stillRunning: boolean): boolean;
12
+ export declare function handoffWindowsTrayForUpdate(
13
+ status: { installed?: boolean; running?: boolean },
14
+ io: {
15
+ stop: () => { exitStatus: number | null; running: boolean };
16
+ start: () => unknown;
17
+ },
18
+ ): WindowsTrayUpdatePlan;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shared, side-effect-free contract for preserving the Windows tray across all
3
+ * updater entry points (npm launcher, CLI updater, and GUI worker).
4
+ */
5
+ export function planWindowsTrayUpdate(status) {
6
+ const installed = status?.installed === true;
7
+ const running = status?.running === true;
8
+ return {
9
+ installed,
10
+ running,
11
+ stopBeforeReplacement: running,
12
+ restoreOnFailure: running,
13
+ refreshAfterReplacement: installed,
14
+ installArgs: ["tray", "install", ...(!running ? ["--no-start"] : [])],
15
+ };
16
+ }
17
+
18
+ export function windowsTrayStopConfirmed(exitStatus, stillRunning) {
19
+ return exitStatus === 0 && !stillRunning;
20
+ }
21
+
22
+ /** Stop before replacement and restore best-effort when the handoff cannot be confirmed. */
23
+ export function handoffWindowsTrayForUpdate(status, io) {
24
+ const plan = planWindowsTrayUpdate(status);
25
+ if (!plan.stopBeforeReplacement) return plan;
26
+ try {
27
+ const stopped = io.stop();
28
+ if (!windowsTrayStopConfirmed(stopped.exitStatus, stopped.running)) {
29
+ throw new Error("the tray still reports running after shutdown");
30
+ }
31
+ return plan;
32
+ } catch (error) {
33
+ if (plan.restoreOnFailure) {
34
+ try { io.start(); } catch { /* preserve the handoff failure */ }
35
+ }
36
+ throw error;
37
+ }
38
+ }
package/src/usage/cost.ts CHANGED
Binary file
@@ -38,6 +38,10 @@ const KIMI_K27_CODE_HIGHSPEED: Cost4 = { input: 1.9, output: 8, cacheRead: 0.38,
38
38
  const KIMI_K26: Cost4 = { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0.95 };
39
39
  const KIMI_K25: Cost4 = { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0.6 };
40
40
  const QWEN38_ROUTEWAY_TEMPORARY: Cost4 = { input: 1.5, output: 5, cacheRead: 0.15, cacheWrite: 0 };
41
+ // Anthropic official list prices (USD / 1M tokens). Cache write uses the published 5-minute rate.
42
+ const CLAUDE_SONNET_46: Cost4 = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
43
+ const CLAUDE_OPUS_46: Cost4 = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
44
+ const ANTHROPIC_PRICING = "https://platform.claude.com/docs/en/about-claude/pricing (official; 5m cache-write tier)";
41
45
 
42
46
  const GEMINI_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-07-22); cacheWrite=0: storage is billed per-hour, not per-token";
43
47
  const MINIMAX_PRICING = "https://platform.minimax.io/docs/guides/pricing-paygo";
@@ -63,6 +67,7 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
63
67
  { provider: "google-antigravity", modelId: "gemini-3.1-pro", cost4: GEMINI_31_PRO, source: `collapsed base ID ${GEMINI_PRICING}`, verifiedAt: "2026-07-22", status: "verified" },
64
68
  { provider: "google-antigravity", modelId: "gemini-3.1-pro-low", cost4: GEMINI_31_PRO, source: `derived: gemini-3.1-pro (<=200k tier) ${GEMINI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
65
69
  { provider: "google-antigravity", modelId: "gemini-3.1-pro-high", cost4: GEMINI_31_PRO, source: `derived: gemini-3.1-pro (<=200k tier) ${GEMINI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
70
+ { provider: "google-antigravity", modelId: "gemini-pro-agent", cost4: GEMINI_31_PRO, source: `wire id for gemini-3.1-pro high ${GEMINI_PRICING}`, verifiedAt: "2026-07-23", status: "verified-derived" },
66
71
  { provider: "google-antigravity", modelId: "gemini-3.6-flash-low", cost4: GEMINI_36_FLASH, source: `derived: gemini-3.6-flash ${GEMINI_PRICING}`, verifiedAt: "2026-07-22", status: "verified-derived" },
67
72
  { provider: "google-antigravity", modelId: "gemini-3.6-flash-medium", cost4: GEMINI_36_FLASH, source: `derived: gemini-3.6-flash ${GEMINI_PRICING}`, verifiedAt: "2026-07-22", status: "verified-derived" },
68
73
  { provider: "google-antigravity", modelId: "gemini-3.6-flash-high", cost4: GEMINI_36_FLASH, source: `derived: gemini-3.6-flash ${GEMINI_PRICING}`, verifiedAt: "2026-07-22", status: "verified-derived" },
@@ -76,8 +81,10 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
76
81
  { provider: "google-antigravity", modelId: "gemini-3.1-pro-preview", cost4: GEMINI_31_PRO, source: GEMINI_PRICING, verifiedAt: "2026-07-20", status: "verified" },
77
82
  // Antigravity-bundled third-party models — derived from the underlying vendor's
78
83
  // official API price (Antigravity itself bills via subscription quota).
79
- { provider: "google-antigravity", modelId: "claude-sonnet-4-6", cost4: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, source: "derived: anthropic official https://platform.claude.com/docs/en/about-claude/pricing (5m cache-write; 1h is $6)", verifiedAt: "2026-07-20", status: "verified-derived" },
80
- { provider: "google-antigravity", modelId: "claude-opus-4-6-thinking", cost4: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, source: "derived: anthropic official https://platform.claude.com/docs/en/about-claude/pricing (5m cache-write; 1h is $10)", verifiedAt: "2026-07-20", status: "verified-derived" },
84
+ { provider: "google-antigravity", modelId: "claude-sonnet-4-6", cost4: CLAUDE_SONNET_46, source: `anthropic official Claude Sonnet 4.6 ${ANTHROPIC_PRICING}`, verifiedAt: "2026-07-23", status: "verified" },
85
+ { provider: "google-antigravity", modelId: "claude-opus-4-6-thinking", cost4: CLAUDE_OPUS_46, source: `anthropic official Claude Opus 4.6 ${ANTHROPIC_PRICING}`, verifiedAt: "2026-07-23", status: "verified" },
86
+ // Alias without the Antigravity "-thinking" suffix (if logs/UI ever surface it).
87
+ { provider: "google-antigravity", modelId: "claude-opus-4-6", cost4: CLAUDE_OPUS_46, source: `anthropic official Claude Opus 4.6 ${ANTHROPIC_PRICING}`, verifiedAt: "2026-07-23", status: "verified" },
81
88
  { provider: "google-antigravity", modelId: "gpt-oss-120b-medium", cost4: { input: 0.03, output: 0.15, cacheRead: 0, cacheWrite: 0 }, source: "derived: gpt-oss-120b open-weights — OpenRouter advertised lowest https://openrouter.ai/openai/gpt-oss-120b/providers", verifiedAt: "2026-07-20", status: "verified-derived" },
82
89
  // Kimi / Moonshot — official price tables are now published (2026-07-20 re-check;
83
90
  // previously empty). kimi = Kimi Code OAuth surface, moonshot = CN key surface,
@@ -1,4 +1,5 @@
1
1
  import { baseProviderLabel } from "../providers/label";
2
+ import { canonicalAntigravityUsageModel } from "../providers/antigravity-models";
2
3
  import { usageDisplayTotalTokens } from "./totals";
3
4
  import type { PersistedUsageEntry, UsageStatus } from "./log";
4
5
  import { estimateComboCost, estimateRequestCost } from "./cost";
@@ -160,13 +161,47 @@ interface UsageAttribution {
160
161
  totalTokens?: number;
161
162
  }
162
163
 
164
+
165
+ /**
166
+ * Usage row identity for model breakdowns.
167
+ * Google Antigravity collapses wire/compat/suffix ids to picker/call base models so
168
+ * historical effort-variant logs merge with current base-model invocations.
169
+ */
170
+ function usageModelIdentity(
171
+ provider: string,
172
+ model: string,
173
+ resolvedModel?: string,
174
+ ): { model: string; resolvedModel?: string } {
175
+ if (baseProviderLabel(provider) !== "google-antigravity") {
176
+ return resolvedModel ? { model, resolvedModel } : { model };
177
+ }
178
+ const fromModel = canonicalAntigravityUsageModel(model);
179
+ const fromResolved = resolvedModel
180
+ ? canonicalAntigravityUsageModel(resolvedModel)
181
+ : undefined;
182
+ // Prefer an explicit base mapping from model; if model is unknown but resolved maps
183
+ // to a known base, use that (covers base call + upstream wire resolvedModel pairs).
184
+ const canonical = fromModel !== model
185
+ ? fromModel
186
+ : (fromResolved && fromResolved !== resolvedModel ? fromResolved : fromModel);
187
+ return { model: canonical };
188
+ }
189
+
190
+ function usageModelKey(providerKey: string, model: string): string {
191
+ return `${providerKey}/${model}`;
192
+ }
193
+
194
+ function antigravityUsageModel(provider: string, model: string): string {
195
+ if (baseProviderLabel(provider) !== "google-antigravity") return model;
196
+ return canonicalAntigravityUsageModel(model);
197
+ }
198
+
163
199
  function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[] {
164
200
  if (!entry.attempts?.length) {
165
201
  return [{
166
202
  requestId: entry.requestId,
167
203
  provider: entry.provider,
168
- model: entry.model,
169
- ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
204
+ ...usageModelIdentity(entry.provider, entry.model, entry.resolvedModel),
170
205
  usageStatus: entry.usageStatus,
171
206
  ...(entry.usage ? { usage: entry.usage } : {}),
172
207
  ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
@@ -175,7 +210,7 @@ function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[] {
175
210
  return entry.attempts.map(attempt => ({
176
211
  requestId: entry.requestId,
177
212
  provider: attempt.provider,
178
- model: attempt.model,
213
+ ...usageModelIdentity(attempt.provider, attempt.model),
179
214
  usageStatus: attempt.usageStatus,
180
215
  ...(attempt.usage ? { usage: attempt.usage } : {}),
181
216
  ...(attempt.totalTokens !== undefined ? { totalTokens: attempt.totalTokens } : {}),
@@ -263,7 +298,7 @@ function buildDayGrid(range: UsageRange, since: number | null, now: number, entr
263
298
  let models = dayModels.get(dayKey);
264
299
  if (!models) { models = new Map(); dayModels.set(dayKey, models); }
265
300
  const providerKey = baseProviderLabel(attribution.provider);
266
- const mKey = `${providerKey}/${attribution.model}`;
301
+ const mKey = usageModelKey(providerKey, attribution.model);
267
302
  let m = models.get(mKey);
268
303
  if (!m) {
269
304
  m = { model: attribution.model, provider: providerKey, requests: 0, attemptCount: 0, totalTokens: 0 };
@@ -310,7 +345,7 @@ function buildModels(entries: PersistedUsageEntry[], totalTokens: number): Usage
310
345
  for (const attribution of usageAttributions(entry)) {
311
346
  const providerKey = baseProviderLabel(attribution.provider);
312
347
  // resolvedModel is a routing detail, not a row identity.
313
- const key = `${providerKey}${attribution.model}`;
348
+ const key = usageModelKey(providerKey, attribution.model);
314
349
  let model = byKey.get(key);
315
350
  if (!model) {
316
351
  model = {
@@ -363,14 +398,14 @@ function buildModels(entries: PersistedUsageEntry[], totalTokens: number): Usage
363
398
  // Combo: attribute each attempt's cost to its own model
364
399
  for (const attemptEst of estimate.attempts) {
365
400
  const aProviderKey = baseProviderLabel(attemptEst.provider);
366
- const aKey = `${aProviderKey}${attemptEst.model}`;
401
+ const aKey = usageModelKey(aProviderKey, antigravityUsageModel(attemptEst.provider, attemptEst.model));
367
402
  const m = byKey.get(aKey);
368
403
  if (m) m.estimatedCostUsd = (m.estimatedCostUsd ?? 0) + attemptEst.cost.total;
369
404
  }
370
405
  } else {
371
406
  // Single-target: attribute to the entry's model
372
407
  const providerKey = baseProviderLabel(entry.provider);
373
- const key = `${providerKey}${entry.model}`;
408
+ const key = usageModelKey(providerKey, antigravityUsageModel(entry.provider, entry.model));
374
409
  const m = byKey.get(key);
375
410
  if (m) m.estimatedCostUsd = (m.estimatedCostUsd ?? 0) + estimate.cost.total;
376
411
  }