@bitkyc08/opencodex 2.7.40-preview.20260725 → 2.7.41-preview.20260726
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/README.md +63 -12
- package/assets/claude-code-models.gif +0 -0
- package/gui/dist/assets/index-7RXBHYrB.js +65 -0
- package/gui/dist/assets/index-B2J4t3te.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/adapters/google.ts +48 -9
- package/src/adapters/kiro-events.ts +15 -3
- package/src/adapters/kiro.ts +292 -28
- package/src/adapters/openai-chat.ts +63 -15
- package/src/adapters/openai-responses.ts +18 -0
- package/src/bridge.ts +76 -21
- package/src/chat/outbound.ts +66 -34
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p.ts +121 -21
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/inbound-debug.ts +4 -0
- package/src/claude/model-info.ts +9 -3
- package/src/cli/account-extended.ts +34 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/claude-desktop.ts +152 -0
- package/src/cli/claude.ts +37 -3
- package/src/cli/doctor.ts +187 -6
- package/src/cli/help.ts +13 -1
- package/src/cli/index.ts +151 -57
- package/src/cli/status-oauth.ts +68 -0
- package/src/cli/status.ts +4 -0
- package/src/codex/account-lifecycle.ts +31 -0
- package/src/codex/auth-api.ts +133 -26
- package/src/codex/auth-collision.ts +55 -11
- package/src/codex/auth-context.ts +50 -6
- package/src/codex/catalog/provider-fetch.ts +6 -2
- package/src/codex/home.ts +61 -1
- package/src/codex/inject.ts +28 -66
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +39 -3
- package/src/codex/main-account-cache.ts +25 -0
- package/src/codex/model-cache.ts +20 -1
- package/src/codex/paths.ts +5 -0
- package/src/codex/routing.ts +138 -11
- package/src/codex/subagent-model-fallback.ts +455 -0
- package/src/codex/sync.ts +17 -0
- package/src/combos/failover.ts +10 -2
- package/src/combos/index.ts +1 -0
- package/src/combos/types.ts +9 -0
- package/src/config.ts +117 -0
- package/src/grok/inject.ts +339 -0
- package/src/grok/status.ts +88 -0
- package/src/grok/sync.ts +66 -0
- package/src/lib/destination-policy.ts +13 -0
- package/src/lib/errors.ts +59 -0
- package/src/lib/privacy.ts +9 -0
- package/src/lib/process-control.ts +57 -4
- package/src/oauth/health.ts +375 -0
- package/src/oauth/index.ts +69 -22
- package/src/oauth/kiro.ts +5 -3
- package/src/oauth/log.ts +48 -0
- package/src/oauth/store.ts +55 -6
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +143 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/api-keys.ts +5 -5
- package/src/providers/derive.ts +22 -1
- package/src/providers/free-directory.ts +181 -0
- package/src/providers/key-failover.ts +2 -2
- package/src/providers/kiro-models.ts +3 -2
- package/src/providers/openai-tiers.ts +1 -1
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/registry.ts +49 -2
- package/src/responses/parser.ts +49 -20
- package/src/responses/state.ts +156 -2
- package/src/router.ts +73 -5
- package/src/server/chat-completions.ts +78 -22
- package/src/server/claude-messages.ts +8 -0
- package/src/server/images.ts +2 -1
- package/src/server/index.ts +50 -30
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +337 -18
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/combo-routes.ts +3 -3
- package/src/server/management/config-routes.ts +4 -4
- package/src/server/management/logs-usage-routes.ts +64 -4
- package/src/server/management/model-routes.ts +114 -8
- package/src/server/management/oauth-account-routes.ts +31 -5
- package/src/server/management/provider-routes.ts +42 -11
- package/src/server/management/shared.ts +81 -2
- package/src/server/management/system-routes.ts +6 -1
- package/src/server/management-api.ts +24 -7
- package/src/server/port-reclaim.ts +261 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/collaboration.ts +11 -6
- package/src/server/responses/compact.ts +2 -1
- package/src/server/responses/core.ts +358 -155
- package/src/server/responses/passthrough-error.ts +53 -0
- package/src/server/search.ts +2 -1
- package/src/server/system-env.ts +23 -6
- package/src/server/windows-tcp-drop.ts +174 -0
- package/src/service.ts +43 -3
- package/src/types.ts +74 -6
- package/src/update/index.ts +30 -19
- package/src/update/job.ts +30 -15
- package/src/usage/log.ts +158 -2
- package/src/usage/summary.ts +8 -4
- package/src/web-search/loop.ts +4 -1
- package/gui/dist/assets/index-BxQ8N_K5.js +0 -52
- package/gui/dist/assets/index-CMip1DzF.css +0 -1
package/src/update/job.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { atomicWriteFile, getConfigDir, loadConfig, readPid, readRuntimePort } from "../config";
|
|
6
6
|
import { killProxy } from "../lib/process-control";
|
|
7
|
-
import {
|
|
7
|
+
import { reclaimListenPort } from "../server/port-reclaim";
|
|
8
8
|
import { proxyIdentityAt } from "../server/proxy-liveness";
|
|
9
9
|
import { isServiceInstalled } from "../service";
|
|
10
10
|
import {
|
|
@@ -28,6 +28,8 @@ const UPDATE_TIMEOUT_MS = 180_000;
|
|
|
28
28
|
const RESTART_TIMEOUT_MS = 60_000;
|
|
29
29
|
const RESTART_HEALTH_TIMEOUT_MS = 15_000;
|
|
30
30
|
const RESTART_STABILITY_WINDOW_MS = 15_000;
|
|
31
|
+
/** How long update restart waits for the captured port to become bindable after stop. */
|
|
32
|
+
export const RESTART_PORT_RECLAIM_MS = 30_000;
|
|
31
33
|
|
|
32
34
|
export type UpdateJobStatus = "running" | "restarting" | "succeeded" | "failed";
|
|
33
35
|
|
|
@@ -292,7 +294,7 @@ function spawnDetachedStart(job: UpdateJobState, installer: Installer, port?: nu
|
|
|
292
294
|
|
|
293
295
|
/** Test seam: the wait/spawn pair is injectable so the restart path is verifiable. */
|
|
294
296
|
export interface RestartIo {
|
|
295
|
-
waitForPort?: typeof
|
|
297
|
+
waitForPort?: typeof reclaimListenPort;
|
|
296
298
|
spawnStart?: (job: UpdateJobState, installer: Installer, port?: number) => void;
|
|
297
299
|
serviceInstalledFn?: () => boolean;
|
|
298
300
|
probeProxy?: (port: number, hostname?: string) => Promise<boolean>;
|
|
@@ -308,7 +310,7 @@ export interface RestartIo {
|
|
|
308
310
|
|
|
309
311
|
async function restartAfterUpdate(
|
|
310
312
|
job: UpdateJobState,
|
|
311
|
-
captured?: { port: number; hostname: string },
|
|
313
|
+
captured?: { port: number; hostname: string; oldPid?: number },
|
|
312
314
|
io: RestartIo = {},
|
|
313
315
|
): Promise<void> {
|
|
314
316
|
const serviceInstalled = (io.serviceInstalledFn ?? isServiceInstalled)();
|
|
@@ -318,6 +320,9 @@ async function restartAfterUpdate(
|
|
|
318
320
|
// port to wait on; config is only the cold-start fallback.
|
|
319
321
|
const port = captured?.port ?? config.port ?? 10100;
|
|
320
322
|
const hostname = captured?.hostname ?? config.hostname ?? "127.0.0.1";
|
|
323
|
+
const oldPid = typeof captured?.oldPid === "number" && captured.oldPid > 0
|
|
324
|
+
? captured.oldPid
|
|
325
|
+
: undefined;
|
|
321
326
|
let svcArgs: string[] | undefined;
|
|
322
327
|
if (serviceInstalled) {
|
|
323
328
|
try {
|
|
@@ -326,14 +331,21 @@ async function restartAfterUpdate(
|
|
|
326
331
|
} catch { /* fallback to default service install */ }
|
|
327
332
|
}
|
|
328
333
|
const cmd = restartCommand(serviceInstalled, job.installer, packageLauncherPath(), port, svcArgs);
|
|
329
|
-
const waitFn = io.waitForPort ??
|
|
334
|
+
const waitFn = io.waitForPort ?? reclaimListenPort;
|
|
335
|
+
const reclaimOpts = {
|
|
336
|
+
timeoutMs: RESTART_PORT_RECLAIM_MS,
|
|
337
|
+
intervalMs: 100,
|
|
338
|
+
scanIntervalMs: 500,
|
|
339
|
+
killOcxHolders: oldPid != null,
|
|
340
|
+
onlyKillPids: oldPid != null ? [oldPid] : [],
|
|
341
|
+
};
|
|
330
342
|
|
|
331
343
|
if (serviceInstalled) {
|
|
332
|
-
// Stop-first update already unloaded the service;
|
|
333
|
-
// then reinstall wrappers that bake `--port
|
|
334
|
-
const freed = await waitFn(port, hostname,
|
|
344
|
+
// Stop-first update already unloaded the service; reclaim the socket (only the
|
|
345
|
+
// captured old PID when trusted), then reinstall wrappers that bake `--port`.
|
|
346
|
+
const freed = await waitFn(port, hostname, reclaimOpts);
|
|
335
347
|
if (!freed) {
|
|
336
|
-
updateJob(job, {}, `Port ${port} still busy after
|
|
348
|
+
updateJob(job, {}, `Port ${port} still busy after ${Math.trunc(RESTART_PORT_RECLAIM_MS / 1000)}s; refusing to hop — reinstall may fail until the port is free.`);
|
|
337
349
|
}
|
|
338
350
|
const prevBake = process.env.OCX_BAKE_PORT;
|
|
339
351
|
process.env.OCX_BAKE_PORT = String(Math.trunc(port));
|
|
@@ -365,12 +377,13 @@ async function restartAfterUpdate(
|
|
|
365
377
|
updateJob(job, {}, `Stopping current proxy PID ${pid}.`);
|
|
366
378
|
killProxy(pid);
|
|
367
379
|
}
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
const freed = await waitFn(port, hostname,
|
|
380
|
+
// Reclaim the captured port before the pinned start. Spawning `--port` while the old
|
|
381
|
+
// socket is still busy is how Windows updates used to fail health checks (or hop).
|
|
382
|
+
// Only the trusted pre-update PID may be killed; never an arbitrary ocx listener.
|
|
383
|
+
const freed = await waitFn(port, hostname, reclaimOpts);
|
|
372
384
|
if (!freed) {
|
|
373
|
-
updateJob(job, {}, `Port ${port} still busy after
|
|
385
|
+
updateJob(job, {}, `Port ${port} still busy after ${Math.trunc(RESTART_PORT_RECLAIM_MS / 1000)}s (reclaim could not free the socket); not starting on another port. Retry 'ocx start --port ${port}'.`);
|
|
386
|
+
return;
|
|
374
387
|
}
|
|
375
388
|
(io.spawnStart ?? spawnDetachedStart)(job, job.installer, port);
|
|
376
389
|
}
|
|
@@ -378,7 +391,7 @@ async function restartAfterUpdate(
|
|
|
378
391
|
/** Exposed for tests: drives the non-service restart path with injected io. */
|
|
379
392
|
export function restartAfterUpdateForTests(
|
|
380
393
|
job: UpdateJobState,
|
|
381
|
-
captured: { port: number; hostname: string },
|
|
394
|
+
captured: { port: number; hostname: string; oldPid?: number },
|
|
382
395
|
io: RestartIo,
|
|
383
396
|
): Promise<void> {
|
|
384
397
|
return restartAfterUpdate(job, captured, io);
|
|
@@ -386,7 +399,8 @@ export function restartAfterUpdateForTests(
|
|
|
386
399
|
|
|
387
400
|
function restartFailureHint(port: number): string {
|
|
388
401
|
return `Update installed, but the restarted proxy did not stay healthy on port ${port}. `
|
|
389
|
-
+
|
|
402
|
+
+ `Try 'ocx start --port ${port}'. `
|
|
403
|
+
+ "If the update log shows bun postinstall or EPERM warnings, "
|
|
390
404
|
+ "reinstall with 'npm install -g --allow-scripts=bun @bitkyc08/opencodex'.";
|
|
391
405
|
}
|
|
392
406
|
|
|
@@ -474,6 +488,7 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar
|
|
|
474
488
|
const captured = {
|
|
475
489
|
port: runtimeTrusted ? rt.port : configPort,
|
|
476
490
|
hostname: (runtimeTrusted ? rt.hostname : undefined) ?? preUpdateConfig.hostname ?? "127.0.0.1",
|
|
491
|
+
...(runtimeTrusted && livePid ? { oldPid: livePid } : {}),
|
|
477
492
|
};
|
|
478
493
|
let trayWasInstalled = false;
|
|
479
494
|
let trayWasRunning = false;
|
package/src/usage/log.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface PersistedUsageEntry {
|
|
|
36
36
|
timestamp: number;
|
|
37
37
|
provider: string;
|
|
38
38
|
model: string;
|
|
39
|
-
surface?: "claude";
|
|
39
|
+
surface?: "claude" | "claude-desktop" | "grok";
|
|
40
40
|
resolvedModel?: string;
|
|
41
41
|
requestedModel?: string;
|
|
42
42
|
/** Reasoning effort / service-tier metadata for GUI Logs after restart. */
|
|
@@ -64,6 +64,23 @@ export interface PersistedUsageEntry {
|
|
|
64
64
|
upstreamError?: string;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
const KNOWN_USAGE_SURFACES = new Set<NonNullable<PersistedUsageEntry["surface"]>>([
|
|
68
|
+
"claude",
|
|
69
|
+
"claude-desktop",
|
|
70
|
+
"grok",
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The serializer guard for `surface`. Two failure modes shaped this: a literal
|
|
75
|
+
* whitelist ("claude" | "claude-desktop" only) silently dropped every NEW surface at
|
|
76
|
+
* write time, while a plain truthy spread would persist junk values from hand-edited
|
|
77
|
+
* logs. Membership in this set is the middle path: adding a surface here is one edit,
|
|
78
|
+
* and unknown values are still dropped.
|
|
79
|
+
*/
|
|
80
|
+
export function isKnownUsageSurface(value: unknown): value is NonNullable<PersistedUsageEntry["surface"]> {
|
|
81
|
+
return typeof value === "string" && KNOWN_USAGE_SURFACES.has(value as NonNullable<PersistedUsageEntry["surface"]>);
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
export function usageLogPath(): string {
|
|
68
85
|
return join(getConfigDir(), "usage.jsonl");
|
|
69
86
|
}
|
|
@@ -217,7 +234,7 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
217
234
|
timestamp: entry.timestamp,
|
|
218
235
|
provider: entry.provider,
|
|
219
236
|
model: entry.model,
|
|
220
|
-
...(entry.surface
|
|
237
|
+
...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
|
|
221
238
|
...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
|
|
222
239
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
223
240
|
...(typeof entry.requestedEffort === "string" && entry.requestedEffort
|
|
@@ -270,6 +287,145 @@ export function appendUsageEntry(entry: PersistedUsageEntry): void {
|
|
|
270
287
|
try { chmodSync(path, 0o600); } catch { /* best-effort on platforms that ignore chmod */ }
|
|
271
288
|
}
|
|
272
289
|
|
|
290
|
+
export type UsageLogRevision = {
|
|
291
|
+
path: string;
|
|
292
|
+
dev: number;
|
|
293
|
+
ino: number;
|
|
294
|
+
birthtimeMs: number;
|
|
295
|
+
size: number;
|
|
296
|
+
mtimeMs: number;
|
|
297
|
+
ctimeMs: number;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
let usageReadCacheStats = { fullReads: 0, tailReads: 0, parsedLines: 0 };
|
|
301
|
+
let managementUsageReadInflight: {
|
|
302
|
+
key: string;
|
|
303
|
+
promise: Promise<{ entries: PersistedUsageEntry[]; revision: UsageLogRevision }>;
|
|
304
|
+
} | null = null;
|
|
305
|
+
|
|
306
|
+
/** Test-only observability for proving that unchanged prefixes are not reparsed. */
|
|
307
|
+
export function usageReadCacheStatsForTests(): Readonly<typeof usageReadCacheStats> {
|
|
308
|
+
return { ...usageReadCacheStats };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function resetUsageReadCacheForTests(): void {
|
|
312
|
+
usageReadCacheStats = { fullReads: 0, tailReads: 0, parsedLines: 0 };
|
|
313
|
+
managementUsageReadInflight = null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function readExactly(fd: number, length: number, position: number): Buffer | null {
|
|
317
|
+
const output = Buffer.allocUnsafe(length);
|
|
318
|
+
let offset = 0;
|
|
319
|
+
while (offset < length) {
|
|
320
|
+
const read = readSync(fd, output, offset, length - offset, position + offset);
|
|
321
|
+
if (read === 0) return null;
|
|
322
|
+
offset += read;
|
|
323
|
+
}
|
|
324
|
+
return output;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function usageLogRevision(path: string, stat: ReturnType<typeof fstatSync>): UsageLogRevision {
|
|
328
|
+
if (!stat.isFile()) throw new Error("usage log is not a regular file");
|
|
329
|
+
return {
|
|
330
|
+
path,
|
|
331
|
+
dev: Number(stat.dev),
|
|
332
|
+
ino: Number(stat.ino),
|
|
333
|
+
birthtimeMs: Number(stat.birthtimeMs),
|
|
334
|
+
size: Number(stat.size),
|
|
335
|
+
mtimeMs: Number(stat.mtimeMs),
|
|
336
|
+
ctimeMs: Number(stat.ctimeMs),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function usageLogRevisionKey(revision: UsageLogRevision | null): string {
|
|
341
|
+
if (!revision) return "missing";
|
|
342
|
+
return [
|
|
343
|
+
revision.path,
|
|
344
|
+
revision.dev,
|
|
345
|
+
revision.ino,
|
|
346
|
+
revision.birthtimeMs,
|
|
347
|
+
revision.size,
|
|
348
|
+
revision.mtimeMs,
|
|
349
|
+
revision.ctimeMs,
|
|
350
|
+
].join("\0");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function currentUsageLogRevision(): UsageLogRevision | null {
|
|
354
|
+
const path = usageLogPath();
|
|
355
|
+
if (!existsSync(path)) return null;
|
|
356
|
+
let fd: number | undefined;
|
|
357
|
+
try {
|
|
358
|
+
fd = openSync(path, "r");
|
|
359
|
+
return usageLogRevision(path, fstatSync(fd));
|
|
360
|
+
} finally {
|
|
361
|
+
if (fd !== undefined) closeSync(fd);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function parseUsageTextCooperatively(text: string): Promise<PersistedUsageEntry[]> {
|
|
366
|
+
const lines = text.split(/\r?\n/);
|
|
367
|
+
usageReadCacheStats.parsedLines += lines.filter(line => line.trim()).length;
|
|
368
|
+
const entries: PersistedUsageEntry[] = [];
|
|
369
|
+
const batchSize = 1_000;
|
|
370
|
+
for (let offset = 0; offset < lines.length; offset += batchSize) {
|
|
371
|
+
entries.push(...parseUsageLines(lines.slice(offset, offset + batchSize)));
|
|
372
|
+
if (offset + batchSize < lines.length) {
|
|
373
|
+
// JSON parsing dominates large-log startup. Yield between bounded batches so
|
|
374
|
+
// Bun can continue serving health and settings requests on the same thread.
|
|
375
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return entries;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function readUsageEntriesFullCooperatively(
|
|
382
|
+
path: string,
|
|
383
|
+
): Promise<{ entries: PersistedUsageEntry[]; revision: UsageLogRevision }> {
|
|
384
|
+
let fd: number | undefined;
|
|
385
|
+
try {
|
|
386
|
+
fd = openSync(path, "r");
|
|
387
|
+
const stat = fstatSync(fd);
|
|
388
|
+
const size = Number(stat.size);
|
|
389
|
+
const bytes = readExactly(fd, size, 0);
|
|
390
|
+
if (bytes === null) throw new Error("usage log changed while it was being read");
|
|
391
|
+
const entries = await parseUsageTextCooperatively(bytes.toString("utf-8"));
|
|
392
|
+
usageReadCacheStats.fullReads += 1;
|
|
393
|
+
return { entries, revision: usageLogRevision(path, stat) };
|
|
394
|
+
} finally {
|
|
395
|
+
if (fd !== undefined) closeSync(fd);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Management API reader: full parses yield between bounded batches and concurrent
|
|
401
|
+
* callers share work only when they observed the same exact file revision. Parsed rows
|
|
402
|
+
* are returned to the request and never retained in module state.
|
|
403
|
+
*/
|
|
404
|
+
export async function readUsageSnapshotForManagement(): Promise<{
|
|
405
|
+
entries: PersistedUsageEntry[];
|
|
406
|
+
revision: UsageLogRevision | null;
|
|
407
|
+
}> {
|
|
408
|
+
const path = usageLogPath();
|
|
409
|
+
if (!existsSync(path)) return { entries: [], revision: null };
|
|
410
|
+
const observed = currentUsageLogRevision();
|
|
411
|
+
const key = usageLogRevisionKey(observed);
|
|
412
|
+
if (managementUsageReadInflight?.key === key) {
|
|
413
|
+
const shared = await managementUsageReadInflight.promise;
|
|
414
|
+
return { entries: shared.entries.slice(), revision: shared.revision };
|
|
415
|
+
}
|
|
416
|
+
const promise = readUsageEntriesFullCooperatively(path);
|
|
417
|
+
managementUsageReadInflight = { key, promise };
|
|
418
|
+
try {
|
|
419
|
+
return await promise;
|
|
420
|
+
} finally {
|
|
421
|
+
if (managementUsageReadInflight?.promise === promise) managementUsageReadInflight = null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export async function readUsageEntriesForManagement(): Promise<PersistedUsageEntry[]> {
|
|
426
|
+
return (await readUsageSnapshotForManagement()).entries;
|
|
427
|
+
}
|
|
428
|
+
|
|
273
429
|
export function readUsageEntries(): PersistedUsageEntry[] {
|
|
274
430
|
const path = usageLogPath();
|
|
275
431
|
if (!existsSync(path)) return [];
|
package/src/usage/summary.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { PersistedUsageEntry, UsageStatus } from "./log";
|
|
|
5
5
|
import { estimateComboCost, estimateRequestCost, effectiveServiceTier } from "./cost";
|
|
6
6
|
|
|
7
7
|
export type UsageRange = "7d" | "30d" | "all";
|
|
8
|
-
export type UsageSurface = "all" | "codex" | "claude";
|
|
8
|
+
export type UsageSurface = "all" | "codex" | "claude" | "grok";
|
|
9
9
|
|
|
10
10
|
export interface UsageSummaryTotals {
|
|
11
11
|
requests: number;
|
|
@@ -98,7 +98,7 @@ export function parseRange(input: string | null | undefined): UsageRange {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
export function parseUsageSurface(input: string | null | undefined): UsageSurface {
|
|
101
|
-
if (input === "codex" || input === "claude") return input;
|
|
101
|
+
if (input === "codex" || input === "claude" || input === "grok") return input;
|
|
102
102
|
return "all";
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -491,8 +491,12 @@ export function summarizeUsage(
|
|
|
491
491
|
const { since } = rangeWindow(range, now);
|
|
492
492
|
const filteredEntries = entries.filter(entry => {
|
|
493
493
|
if (since !== null && entry.timestamp < since) return false;
|
|
494
|
-
if (surface === "claude") return entry.surface === "claude";
|
|
495
|
-
if (surface === "
|
|
494
|
+
if (surface === "claude") return entry.surface === "claude" || entry.surface === "claude-desktop";
|
|
495
|
+
if (surface === "grok") return entry.surface === "grok";
|
|
496
|
+
// Codex = the historical unlabelled bucket. Before the grok tag existed every
|
|
497
|
+
// non-Claude turn landed here, and `surface !== "claude"` also swallowed
|
|
498
|
+
// claude-desktop — disjoint predicates fix both.
|
|
499
|
+
if (surface === "codex") return entry.surface === undefined;
|
|
496
500
|
return true;
|
|
497
501
|
});
|
|
498
502
|
const totals = blankTotals();
|
package/src/web-search/loop.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProviderAdapter } from "../adapters/base";
|
|
2
|
-
import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxThinkingContent } from "../types";
|
|
2
|
+
import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxThinkingContent, OcxUsage } from "../types";
|
|
3
3
|
import { namespacedToolName } from "../types";
|
|
4
4
|
import { bridgeToResponsesSSE } from "../bridge";
|
|
5
5
|
import { runWebSearch, type SidecarOutcome, type SidecarOutcomeRecorder, type SidecarSettings } from "./executor";
|
|
@@ -189,6 +189,8 @@ export interface WebSearchLoopDeps {
|
|
|
189
189
|
stallTimeoutSec?: number;
|
|
190
190
|
/** One-shot TTFT callback: first non-empty model output observed (WP4). */
|
|
191
191
|
onFirstOutput?: () => void;
|
|
192
|
+
/** Raw adapter usage at the terminal event, pre wire-normalization (see bridgeToResponsesSSE onUsage). */
|
|
193
|
+
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
192
194
|
/**
|
|
193
195
|
* 429 key-failover hook: rotate the provider's active pool key and return a rebuilt adapter,
|
|
194
196
|
* or null when the pool is exhausted (same semantics as the normal routed path).
|
|
@@ -575,6 +577,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
575
577
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
576
578
|
...(deps.stallTimeoutSec !== undefined ? { stallTimeoutSec: deps.stallTimeoutSec } : {}),
|
|
577
579
|
...(deps.onFirstOutput ? { onFirstOutput: deps.onFirstOutput } : {}),
|
|
580
|
+
...(deps.onUsage ? { onUsage: deps.onUsage } : {}),
|
|
578
581
|
},
|
|
579
582
|
);
|
|
580
583
|
return new Response(sse, { headers: SSE_HEADERS });
|