@bitkyc08/opencodex 2.7.43 → 2.8.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.
- package/bin/ocx.mjs +34 -8
- package/gui/dist/assets/index-BDjpkcRN.js +67 -0
- package/gui/dist/assets/index-BHsKRFh9.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/kiro.ts +15 -1
- package/src/claude/alias.ts +94 -14
- package/src/claude/outbound.ts +6 -3
- package/src/cli/catalog-prewarm.ts +24 -0
- package/src/cli/claude.ts +32 -7
- package/src/cli/doctor.ts +48 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/interactive-confirm.ts +5 -1
- package/src/cli/star-prompt.ts +26 -4
- package/src/cli/v2.ts +10 -1
- package/src/codex/account-store.ts +2 -0
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/parsing.ts +26 -1
- package/src/codex/catalog/provider-fetch.ts +240 -82
- package/src/codex/catalog/sync.ts +27 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/features.ts +524 -5
- package/src/codex/quota.ts +77 -2
- package/src/codex/runtime.ts +10 -1
- package/src/config.ts +8 -0
- package/src/generated/jawcode-model-metadata.ts +12 -12
- package/src/github/star-state.ts +191 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +6 -20
- package/src/lib/destination-policy.ts +10 -3
- package/src/lib/provider-outbound.ts +5 -2
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/win-exec.ts +12 -2
- package/src/oauth/index.ts +29 -5
- package/src/oauth/key-providers.ts +21 -2
- package/src/oauth/kiro-credentials.ts +57 -8
- package/src/oauth/kiro.ts +2 -1
- package/src/oauth/login-cli.ts +1 -1
- package/src/oauth/store.ts +2 -0
- package/src/providers/derive.ts +2 -2
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/registry.ts +114 -0
- package/src/router.ts +5 -3
- package/src/server/auth-cors.ts +4 -2
- package/src/server/live.ts +75 -25
- package/src/server/management/agent-settings-routes.ts +78 -4
- package/src/server/management/config-routes.ts +19 -7
- package/src/server/management/context.ts +11 -1
- package/src/server/management/model-routes.ts +46 -13
- package/src/server/management/provider-routes.ts +44 -9
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +39 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/responses/core.ts +31 -20
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/startup-action-control.ts +30 -14
- package/src/service.ts +237 -19
- package/src/storage/policy-job.ts +26 -5
- package/src/storage/restore-job.ts +16 -5
- package/src/storage/worker-lifecycle.ts +81 -0
- package/src/tray/windows.ts +32 -4
- package/src/types.ts +11 -0
- package/src/update/badge.ts +72 -0
- package/src/update/job.ts +8 -4
- package/src/usage/expected-prices.ts +6 -5
- package/src/usage/log.ts +8 -0
- package/src/web-search/loop.ts +57 -16
- package/gui/dist/assets/index-Czw-jpTU.css +0 -1
- package/gui/dist/assets/index-cmds12BG.js +0 -67
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic teardown for the storage Bun Workers.
|
|
3
|
+
*
|
|
4
|
+
* `Worker.terminate()` returns void and does NOT wait for the thread to be
|
|
5
|
+
* reclaimed. Every caller here used to fire it and move on, which is fine for
|
|
6
|
+
* the proxy but not for `bun test --isolate`: the harness tears a test file's
|
|
7
|
+
* realm down at the file boundary, and on Windows a worker that has not
|
|
8
|
+
* finished exiting by then trips a Bun-internal assertion and kills the whole
|
|
9
|
+
* run.
|
|
10
|
+
*
|
|
11
|
+
* The crash header names the shape exactly — `workers_spawned(9)
|
|
12
|
+
* workers_terminated(8)`, one worker still alive — and it lands right at the
|
|
13
|
+
* `api-storage-policy` → `api-storage` file boundary, the only suite that
|
|
14
|
+
* spawns policy workers. See `devlog/_plan/260730_remote_issue_merge_round/150`,
|
|
15
|
+
* which reproduced the panic twice against an unchanged tree and left this
|
|
16
|
+
* defence as the follow-up if it ever recurred. It recurred.
|
|
17
|
+
*
|
|
18
|
+
* So: keep a registry of live workers, and give shutdown/reset paths something
|
|
19
|
+
* they can actually await. `close` is Bun's post-exit event for a worker
|
|
20
|
+
* thread, so awaiting it is the real "the thread is gone" signal rather than a
|
|
21
|
+
* timer we hope is long enough. The timeout only exists so a wedged worker
|
|
22
|
+
* cannot hang a test teardown forever.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const liveWorkers = new Set<Worker>();
|
|
26
|
+
|
|
27
|
+
/** Track a freshly spawned worker so teardown can wait for it later. */
|
|
28
|
+
export function registerStorageWorker(worker: Worker): void {
|
|
29
|
+
liveWorkers.add(worker);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Terminate a worker and resolve once its thread has actually exited.
|
|
34
|
+
*
|
|
35
|
+
* Safe to call twice: the second call finds the worker already deregistered and
|
|
36
|
+
* resolves immediately.
|
|
37
|
+
*/
|
|
38
|
+
export function terminateStorageWorker(worker: Worker, timeoutMs = 5_000): Promise<void> {
|
|
39
|
+
if (!liveWorkers.has(worker)) {
|
|
40
|
+
try { worker.terminate(); } catch { /* already gone */ }
|
|
41
|
+
return Promise.resolve();
|
|
42
|
+
}
|
|
43
|
+
liveWorkers.delete(worker);
|
|
44
|
+
|
|
45
|
+
return new Promise<void>(resolve => {
|
|
46
|
+
let done = false;
|
|
47
|
+
const settle = (): void => {
|
|
48
|
+
if (done) return;
|
|
49
|
+
done = true;
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
resolve();
|
|
52
|
+
};
|
|
53
|
+
// A worker that refuses to exit must not wedge teardown; the proxy path
|
|
54
|
+
// never awaits this, and a test teardown would rather continue than hang.
|
|
55
|
+
const timer = setTimeout(settle, timeoutMs);
|
|
56
|
+
try {
|
|
57
|
+
worker.addEventListener("close", settle, { once: true });
|
|
58
|
+
} catch {
|
|
59
|
+
// No close event available — fall back to the timeout above.
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
worker.terminate();
|
|
63
|
+
} catch {
|
|
64
|
+
settle();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Await every worker this module still tracks. Used by test resets so no
|
|
71
|
+
* storage worker outlives the file that spawned it.
|
|
72
|
+
*/
|
|
73
|
+
export async function drainStorageWorkers(timeoutMs = 5_000): Promise<void> {
|
|
74
|
+
const pending = [...liveWorkers];
|
|
75
|
+
await Promise.all(pending.map(worker => terminateStorageWorker(worker, timeoutMs)));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Live worker count — exported so a regression test can assert the invariant. */
|
|
79
|
+
export function liveStorageWorkerCount(): number {
|
|
80
|
+
return liveWorkers.size;
|
|
81
|
+
}
|
package/src/tray/windows.ts
CHANGED
|
@@ -414,17 +414,43 @@ function assertWindows(): void {
|
|
|
414
414
|
if (process.platform !== "win32") throw new Error(`The opencodex tray is Windows-only (current platform: ${process.platform}).`);
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
417
|
+
const DETACHED_TRAY_HOST_LAUNCHER = [
|
|
418
|
+
"$startInfo = New-Object System.Diagnostics.ProcessStartInfo",
|
|
419
|
+
"$startInfo.FileName = $env:OCX_TRAY_HOST_BUN",
|
|
420
|
+
"$startInfo.Arguments = $env:OCX_TRAY_HOST_ARGS",
|
|
421
|
+
"$startInfo.UseShellExecute = $true",
|
|
422
|
+
"$startInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden",
|
|
423
|
+
"$child = [System.Diagnostics.Process]::Start($startInfo)",
|
|
424
|
+
"if ($null -eq $child) { throw 'Windows tray host did not start.' }",
|
|
425
|
+
"$child.Dispose()",
|
|
426
|
+
].join("; ");
|
|
427
|
+
|
|
428
|
+
const DETACHED_TRAY_HOST_LAUNCHER_B64 = Buffer.from(DETACHED_TRAY_HOST_LAUNCHER, "utf16le").toString("base64");
|
|
429
|
+
|
|
430
|
+
export function launchWindowsTrayHost(state: WindowsTrayEntry): void {
|
|
431
|
+
const bun = safePath(state.bun);
|
|
432
|
+
const cli = safePath(state.cli);
|
|
433
|
+
execFileSync(windowsPowerShellPath(), [
|
|
434
|
+
"-NoLogo",
|
|
435
|
+
"-NoProfile",
|
|
436
|
+
"-NonInteractive",
|
|
437
|
+
"-EncodedCommand",
|
|
438
|
+
DETACHED_TRAY_HOST_LAUNCHER_B64,
|
|
439
|
+
], {
|
|
420
440
|
stdio: "ignore",
|
|
421
441
|
windowsHide: true,
|
|
442
|
+
timeout: 15_000,
|
|
422
443
|
env: {
|
|
423
444
|
...process.env,
|
|
445
|
+
OCX_TRAY_HOST_BUN: bun,
|
|
446
|
+
OCX_TRAY_HOST_ARGS: `${quoteRunValue(cli)} __tray-host`,
|
|
424
447
|
OCX_TRAY_ENTRY_B64: Buffer.from(JSON.stringify(state), "utf8").toString("base64"),
|
|
425
448
|
},
|
|
426
449
|
});
|
|
427
|
-
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function spawnTray(state: WindowsTrayEntry): void {
|
|
453
|
+
launchWindowsTrayHost(state);
|
|
428
454
|
}
|
|
429
455
|
|
|
430
456
|
function parseTrayHostEntry(): WindowsTrayEntry {
|
|
@@ -443,6 +469,8 @@ export async function runWindowsTrayHost(): Promise<void> {
|
|
|
443
469
|
assertWindows();
|
|
444
470
|
const entry = parseTrayHostEntry();
|
|
445
471
|
delete process.env.OCX_TRAY_ENTRY_B64;
|
|
472
|
+
delete process.env.OCX_TRAY_HOST_BUN;
|
|
473
|
+
delete process.env.OCX_TRAY_HOST_ARGS;
|
|
446
474
|
const child = spawn(windowsPowerShellPath(), windowsTrayProcessArgs(entry, "Run", process.pid), {
|
|
447
475
|
stdio: "ignore",
|
|
448
476
|
windowsHide: true,
|
package/src/types.ts
CHANGED
|
@@ -546,6 +546,17 @@ export interface OcxConfig {
|
|
|
546
546
|
* the Codex ladder (src/reasoning-effort.ts CODEX_REASONING_LEVELS) at the API boundary.
|
|
547
547
|
*/
|
|
548
548
|
injectionEffort?: string;
|
|
549
|
+
/**
|
|
550
|
+
* Explicit sideband websocket base for realtime/live joins, mirroring upstream's
|
|
551
|
+
* `experimental_realtime_ws_base_url`. The value is a ROOT (or a recognized
|
|
552
|
+
* `/realtime`, `/realtime/calls/<id>`, `/live/<id>` endpoint form, which is
|
|
553
|
+
* stripped back to the root); `/v1` is appended during normalization. Intended
|
|
554
|
+
* for local development against a fake realtime server — plaintext `http`/`ws`
|
|
555
|
+
* is accepted only for loopback hosts, and URL userinfo is rejected; both
|
|
556
|
+
* failures close to the canonical `https://api.openai.com/v1`. Configured by
|
|
557
|
+
* editing this file; there is deliberately no management-API or GUI surface.
|
|
558
|
+
*/
|
|
559
|
+
experimentalRealtimeWsBaseUrl?: string;
|
|
549
560
|
/**
|
|
550
561
|
* Model ids the user has EXCLUDED from the Grok Build managed block. Absent or empty
|
|
551
562
|
* means "everything visible", which is the historical behaviour — so an existing
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cached "is an update available?" answer for the GUI sidebar badge.
|
|
3
|
+
*
|
|
4
|
+
* `/api/update/check` spawns `npm view` on every call (~1s, network-bound), so a
|
|
5
|
+
* sidebar that polls it would spawn a process per tick on every page of the GUI.
|
|
6
|
+
* The badge instead READS the 20h version cache the CLI update prompt already
|
|
7
|
+
* maintains (`~/.opencodex/version.json`).
|
|
8
|
+
*
|
|
9
|
+
* This is deliberately read-only: it must never trigger a registry refresh. The GUI
|
|
10
|
+
* polls it, so a refresh-on-read would let repeated polls launch repeated `npm view`
|
|
11
|
+
* helpers with no coalescing. Cache warming stays with `ocx start`
|
|
12
|
+
* (`triggerBackgroundRefreshIfStale` in `src/update/notify.ts`) and with the explicit
|
|
13
|
+
* `/api/update/check` the user reaches by clicking the sidebar update button.
|
|
14
|
+
*/
|
|
15
|
+
import { currentVersion, defaultUpdateTag, detectInstall, type Channel } from "./index";
|
|
16
|
+
import { isNewer, isSourceBuildVersion, readVersionCache } from "./notify";
|
|
17
|
+
|
|
18
|
+
export interface UpdateBadge {
|
|
19
|
+
/** True only when a newer version exists on the current channel. */
|
|
20
|
+
updateAvailable: boolean;
|
|
21
|
+
currentVersion: string;
|
|
22
|
+
latestVersion: string | null;
|
|
23
|
+
channel: Channel;
|
|
24
|
+
/** False for source checkouts, where the GUI cannot offer a one-click update. */
|
|
25
|
+
canUpdate: boolean;
|
|
26
|
+
/** True when no cached registry answer exists yet, so "no update" is unproven. */
|
|
27
|
+
unknown: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UpdateBadgeDeps {
|
|
31
|
+
currentVersion: () => string;
|
|
32
|
+
detectInstall: () => ReturnType<typeof detectInstall>;
|
|
33
|
+
readCache: (channel: Channel) => ReturnType<typeof readVersionCache>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const defaultDeps: UpdateBadgeDeps = {
|
|
37
|
+
currentVersion,
|
|
38
|
+
detectInstall,
|
|
39
|
+
readCache: readVersionCache,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read-only badge state. Source checkouts and unknown versions report no update
|
|
44
|
+
* rather than a dead badge the user cannot act on.
|
|
45
|
+
*/
|
|
46
|
+
export function readUpdateBadge(deps: UpdateBadgeDeps = defaultDeps): UpdateBadge {
|
|
47
|
+
const current = deps.currentVersion();
|
|
48
|
+
const installer = deps.detectInstall();
|
|
49
|
+
const channel = defaultUpdateTag(current);
|
|
50
|
+
const base: UpdateBadge = {
|
|
51
|
+
updateAvailable: false,
|
|
52
|
+
currentVersion: current,
|
|
53
|
+
latestVersion: null,
|
|
54
|
+
channel,
|
|
55
|
+
canUpdate: installer !== "source",
|
|
56
|
+
unknown: true,
|
|
57
|
+
};
|
|
58
|
+
// A source checkout has nothing to compare against, so "unknown" is not useful there.
|
|
59
|
+
if (installer === "source" || current === "?" || isSourceBuildVersion(current)) {
|
|
60
|
+
return { ...base, canUpdate: false, unknown: false };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cache = deps.readCache(channel);
|
|
64
|
+
if (!cache) return base;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
...base,
|
|
68
|
+
latestVersion: cache.latest_version,
|
|
69
|
+
updateAvailable: isNewer(cache.latest_version, current, channel),
|
|
70
|
+
unknown: false,
|
|
71
|
+
};
|
|
72
|
+
}
|
package/src/update/job.ts
CHANGED
|
@@ -26,7 +26,7 @@ const RELEASE_NOTES_URL = "https://github.com/lidge-jun/opencodex/releases/lates
|
|
|
26
26
|
const UPDATE_JOB_FILENAME = "update-job.json";
|
|
27
27
|
const UPDATE_TIMEOUT_MS = 180_000;
|
|
28
28
|
const RESTART_TIMEOUT_MS = 60_000;
|
|
29
|
-
const RESTART_HEALTH_TIMEOUT_MS =
|
|
29
|
+
const RESTART_HEALTH_TIMEOUT_MS = 30_000;
|
|
30
30
|
const RESTART_STABILITY_WINDOW_MS = 15_000;
|
|
31
31
|
/** Legacy active records did not persist a worker PID, so age is their only safe recovery signal. */
|
|
32
32
|
export const UPDATE_JOB_LEGACY_STALE_MS = 10 * 60_000;
|
|
@@ -533,7 +533,10 @@ async function awaitRestartedProxyHealthy(
|
|
|
533
533
|
const hostname = captured.hostname;
|
|
534
534
|
const startDeadline = now() + RESTART_HEALTH_TIMEOUT_MS;
|
|
535
535
|
|
|
536
|
-
while (
|
|
536
|
+
while (true) {
|
|
537
|
+
// Always make one identity-aware probe at or after the boundary. A replacement
|
|
538
|
+
// becoming healthy on the final tick must not be mistaken for a timeout.
|
|
539
|
+
const finalProbe = now() >= startDeadline;
|
|
537
540
|
if (await probe(port, hostname)) {
|
|
538
541
|
updateJob(job, {}, `Proxy reported healthy on ${hostname}:${port}; confirming it stays up...`);
|
|
539
542
|
const stableUntil = now() + RESTART_STABILITY_WINDOW_MS;
|
|
@@ -547,7 +550,8 @@ async function awaitRestartedProxyHealthy(
|
|
|
547
550
|
updateJob(job, {}, `Proxy stayed healthy for ${Math.trunc(RESTART_STABILITY_WINDOW_MS / 1000)}s after restart.`);
|
|
548
551
|
return { ok: true };
|
|
549
552
|
}
|
|
550
|
-
|
|
553
|
+
if (finalProbe) break;
|
|
554
|
+
await sleep(Math.min(250, Math.max(0, startDeadline - now())));
|
|
551
555
|
}
|
|
552
556
|
|
|
553
557
|
return { ok: false, reason: "timeout" };
|
|
@@ -570,7 +574,7 @@ async function confirmRestartedProxy(
|
|
|
570
574
|
- 검토한 주요 대안: (1) 포트 점유만 확인 — 외부 프로세스/죽기 직전 프로세스를 성공으로 오인할 수 있다. (2) 무기한 /healthz 폴링 — UX가 느려지고 worker 종료 시점이 불명확하다. (3) 짧은 healthy 등장 + 안정성 창 확인 — 실제 복귀를 확인하면서도 대기 시간을 제한할 수 있다.
|
|
571
575
|
- 선택한 방식: identity-aware /healthz probe가 일정 시간 안에 나타나고, 추가 안정성 창 동안 유지되는지 확인한다.
|
|
572
576
|
- 다른 대안 대신 이 방식을 선택한 이유: GUI는 "업데이트가 설치됐지만 재시작은 실패"를 분리해 알려줘야 하며, 이 방식이 가장 적은 오탐으로 그 경계를 만든다.
|
|
573
|
-
- 장점, 단점 및 영향: 장점은 silent restart failure가 update-job 상태로 드러난다는 점이다. 단점은 성공
|
|
577
|
+
- 장점, 단점 및 영향: 장점은 silent restart failure가 update-job 상태로 드러난다는 점이다. 단점은 설정상 성공 판정 창이 30초 도착 + 15초 안정성으로 늘어나고 경계 probe 지연이 추가될 수 있다는 점이며, 대신 실제 복귀를 더 정확히 반영한다.
|
|
574
578
|
*/
|
|
575
579
|
const result = await awaitRestartedProxyHealthy(job, captured, io);
|
|
576
580
|
if (result.ok) return true;
|
|
@@ -144,16 +144,17 @@ export function findExpectedPriceOverlay(
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
|
-
* OpenAI
|
|
148
|
-
* Source: https://
|
|
149
|
-
*
|
|
147
|
+
* OpenAI Fast mode (`service_tier=priority`) price multipliers by model slug.
|
|
148
|
+
* Source: https://openai.com/api-fast-mode/ (2026-07-31).
|
|
149
|
+
* Fast pricing applies uniformly to all token types (input, output, cache).
|
|
150
150
|
* Models not listed here fall back to 1× (no multiplier).
|
|
151
151
|
*/
|
|
152
152
|
export const PRIORITY_MULTIPLIERS: Readonly<Record<string, number>> = {
|
|
153
153
|
"gpt-5.6-sol": 2,
|
|
154
|
-
"gpt-5.6-terra":
|
|
155
|
-
"gpt-5.6-luna":
|
|
154
|
+
"gpt-5.6-terra": 1.6,
|
|
155
|
+
"gpt-5.6-luna": 0.4,
|
|
156
156
|
"gpt-5.5": 2.5,
|
|
157
|
+
"gpt-5.4-mini": 2,
|
|
157
158
|
"gpt-5.4": 2,
|
|
158
159
|
};
|
|
159
160
|
|
package/src/usage/log.ts
CHANGED
|
@@ -128,6 +128,13 @@ function normalizeUsageValue(usage: OcxUsage | undefined): OcxUsage | undefined
|
|
|
128
128
|
return {
|
|
129
129
|
inputTokens: usage.inputTokens,
|
|
130
130
|
outputTokens: usage.outputTokens,
|
|
131
|
+
// Absolute active-context checkpoint (types.ts). Stateful providers such as Kiro report
|
|
132
|
+
// per-attempt usage only, so this field is the ONLY carrier of the cumulative context
|
|
133
|
+
// figure once the log records raw adapter usage instead of re-parsing the bridged wire
|
|
134
|
+
// (usageFromBridge, request-log.ts). Omitting it here silently dropped Kiro's context
|
|
135
|
+
// growth from every persisted row. It is deliberately NOT folded into totalTokens:
|
|
136
|
+
// a checkpoint is not a per-request total and must never be summed across requests.
|
|
137
|
+
...(typeof usage.contextTotalTokens === "number" ? { contextTotalTokens: usage.contextTotalTokens } : {}),
|
|
131
138
|
...(typeof usage.totalTokens === "number" ? { totalTokens: usage.totalTokens } : {}),
|
|
132
139
|
...(typeof usage.cachedInputTokens === "number" ? { cachedInputTokens: usage.cachedInputTokens } : {}),
|
|
133
140
|
...(typeof usage.cacheReadInputTokens === "number" ? { cacheReadInputTokens: usage.cacheReadInputTokens } : {}),
|
|
@@ -162,6 +169,7 @@ function normalizeAttemptUsage(raw: unknown): OcxUsage | null {
|
|
|
162
169
|
if (!isNonNegativeFiniteNumber(usage.inputTokens)
|
|
163
170
|
|| !isNonNegativeFiniteNumber(usage.outputTokens)) return null;
|
|
164
171
|
for (const key of [
|
|
172
|
+
"contextTotalTokens",
|
|
165
173
|
"totalTokens",
|
|
166
174
|
"cachedInputTokens",
|
|
167
175
|
"cacheReadInputTokens",
|
package/src/web-search/loop.ts
CHANGED
|
@@ -103,23 +103,63 @@ async function* replay(events: AdapterEvent[]): AsyncGenerator<AdapterEvent> {
|
|
|
103
103
|
* replaying a bare toolCall 400s ("Expected `thinking` or `redacted_thinking`, but found
|
|
104
104
|
* `tool_use`"). The signature validity gate stays in the anthropic adapter; other adapters
|
|
105
105
|
* ignore or serialize the part harmlessly.
|
|
106
|
+
*
|
|
107
|
+
* Each signed block keeps its OWN signature and text, mirroring src/images/loop.ts: a signature
|
|
108
|
+
* authenticates the exact block it closed, so flattening two blocks under the last signature
|
|
109
|
+
* 400s on replay just as it does there.
|
|
110
|
+
*
|
|
111
|
+
* Raw reasoning (`reasoning_raw_delta`, what OpenAI-compatible providers emit instead of signed
|
|
112
|
+
* thinking) accumulates into a SEPARATE UNSIGNED part. It must never join a signed block: the
|
|
113
|
+
* anthropic serializer skips signature-less parts, while openai-chat serializes their text as
|
|
114
|
+
* `reasoning_content` — which DeepSeek V4 thinking mode requires back alongside the replayed
|
|
115
|
+
* tool_calls, and whose absence ended the turn as a provider 400 (issue #688).
|
|
116
|
+
*
|
|
117
|
+
* This assumes raw reasoning never interleaves INSIDE an unfinished signed block: Anthropic-family
|
|
118
|
+
* adapters emit thinking_delta/signature and OpenAI-compatible ones emit reasoning_raw_delta, and
|
|
119
|
+
* the two never share a stream. Honoring a genuinely mixed stream would need per-segment state,
|
|
120
|
+
* not another accumulator.
|
|
106
121
|
*/
|
|
107
|
-
function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent
|
|
122
|
+
function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[] {
|
|
123
|
+
const parts: OcxThinkingContent[] = [];
|
|
108
124
|
let thinking = "";
|
|
109
125
|
let signature: string | undefined;
|
|
110
|
-
|
|
126
|
+
let rawReasoning = "";
|
|
127
|
+
|
|
128
|
+
const flushVisible = () => {
|
|
129
|
+
if (!thinking && !signature) return;
|
|
130
|
+
parts.push({
|
|
131
|
+
type: "thinking",
|
|
132
|
+
thinking,
|
|
133
|
+
...(signature ? { signature } : {}),
|
|
134
|
+
});
|
|
135
|
+
thinking = "";
|
|
136
|
+
signature = undefined;
|
|
137
|
+
};
|
|
138
|
+
const flushRaw = () => {
|
|
139
|
+
if (!rawReasoning) return;
|
|
140
|
+
parts.push({ type: "thinking", thinking: rawReasoning });
|
|
141
|
+
rawReasoning = "";
|
|
142
|
+
};
|
|
143
|
+
|
|
111
144
|
for (const e of events) {
|
|
112
|
-
if (e.type === "thinking_delta")
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
if (e.type === "thinking_delta") {
|
|
146
|
+
flushRaw();
|
|
147
|
+
thinking += e.thinking;
|
|
148
|
+
} else if (e.type === "reasoning_raw_delta") {
|
|
149
|
+
flushVisible();
|
|
150
|
+
rawReasoning += e.text;
|
|
151
|
+
} else if (e.type === "thinking_signature") {
|
|
152
|
+
signature = e.signature;
|
|
153
|
+
flushVisible();
|
|
154
|
+
} else if (e.type === "redacted_thinking") {
|
|
155
|
+
flushVisible();
|
|
156
|
+
flushRaw();
|
|
157
|
+
parts.push({ type: "thinking", thinking: "", redacted: [e.data] });
|
|
158
|
+
}
|
|
115
159
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
thinking,
|
|
120
|
-
...(signature ? { signature } : {}),
|
|
121
|
-
...(redacted.length > 0 ? { redacted } : {}),
|
|
122
|
-
};
|
|
160
|
+
flushVisible();
|
|
161
|
+
flushRaw();
|
|
162
|
+
return parts;
|
|
123
163
|
}
|
|
124
164
|
|
|
125
165
|
/** Normalize a query for failed-query de-duplication (case/whitespace-insensitive). */
|
|
@@ -406,7 +446,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
406
446
|
// valid, and surface as ONE search cell carrying every attempted query. A real search (one that
|
|
407
447
|
// hits the sidecar) shows the spinner WHILE the batch runs. Empty/limit/repeat placeholders never
|
|
408
448
|
// emit a cell (matching the prior single-query behavior).
|
|
409
|
-
async function* runSearchCall(call: WebSearchCall, precedingThinking
|
|
449
|
+
async function* runSearchCall(call: WebSearchCall, precedingThinking: OcxThinkingContent[] = []): AsyncGenerator<AdapterEvent> {
|
|
410
450
|
const results: { query: string; outcome: SidecarOutcome }[] = [];
|
|
411
451
|
let beganCell = false;
|
|
412
452
|
if (call.queries.length === 0) {
|
|
@@ -465,8 +505,9 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
465
505
|
messages.push({
|
|
466
506
|
role: "assistant",
|
|
467
507
|
content: [
|
|
468
|
-
// Signed thinking must precede tool_use on replay (Anthropic extended thinking)
|
|
469
|
-
|
|
508
|
+
// Signed thinking must precede tool_use on replay (Anthropic extended thinking), and
|
|
509
|
+
// unsigned raw reasoning has to ride along for providers that require it back (#688).
|
|
510
|
+
...precedingThinking,
|
|
470
511
|
{ type: "toolCall" as const, id: call.id, name: WEB_SEARCH_TOOL_NAME, arguments: callArgs },
|
|
471
512
|
],
|
|
472
513
|
timestamp: now,
|
|
@@ -559,7 +600,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
559
600
|
// The thinking that led to the search belongs to the FIRST call's assistant replay turn.
|
|
560
601
|
const iterationThinking = extractIterationThinking(split.passthrough);
|
|
561
602
|
for (const [callIndex, call] of split.calls.entries()) {
|
|
562
|
-
yield* runSearchCall(call, callIndex === 0 ? iterationThinking :
|
|
603
|
+
yield* runSearchCall(call, callIndex === 0 ? iterationThinking : []);
|
|
563
604
|
}
|
|
564
605
|
} catch (e) {
|
|
565
606
|
yield { type: "error", message: e instanceof LoopError ? e.message : (e instanceof Error ? e.message : String(e)) };
|