@bitkyc08/opencodex 2.13.0 → 2.14.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 (55) hide show
  1. package/gui/dist/assets/index-Co12XTT-.js +76 -0
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/cursor/discovery.ts +4 -1
  5. package/src/adapters/cursor/effort-map.ts +5 -1
  6. package/src/adapters/cursor/request-builder.ts +3 -3
  7. package/src/adapters/google.ts +25 -5
  8. package/src/adapters/openai-chat.ts +182 -6
  9. package/src/adapters/openai-responses.ts +17 -7
  10. package/src/codex/catalog/bundled.ts +16 -0
  11. package/src/codex/catalog/metadata.ts +180 -5
  12. package/src/codex/catalog/parsing.ts +7 -6
  13. package/src/codex/catalog/sync.ts +73 -6
  14. package/src/codex/catalog.ts +1 -1
  15. package/src/codex/convergence.ts +20 -0
  16. package/src/codex/prompt-journal.ts +50 -13
  17. package/src/codex/prompt-layers.ts +1 -1
  18. package/src/config.ts +57 -0
  19. package/src/generated/compatibility-version.json +64 -48
  20. package/src/generated/model-metadata.ts +3 -3
  21. package/src/lib/local-provider-reload-contract.ts +100 -0
  22. package/src/oauth/login-cli.ts +52 -26
  23. package/src/providers/derive.ts +2 -0
  24. package/src/providers/openai-sidecar.ts +9 -2
  25. package/src/providers/quota.ts +57 -0
  26. package/src/providers/registry.ts +53 -25
  27. package/src/responses/state.ts +22 -0
  28. package/src/router.ts +1 -0
  29. package/src/server/claude-messages.ts +57 -11
  30. package/src/server/direct-local-http.ts +7 -3
  31. package/src/server/images.ts +6 -0
  32. package/src/server/index.ts +51 -11
  33. package/src/server/live.ts +117 -13
  34. package/src/server/local-provider-reload-client.ts +137 -0
  35. package/src/server/management/config-routes.ts +20 -3
  36. package/src/server/management/logs-usage-routes.ts +28 -0
  37. package/src/server/management/model-routes.ts +11 -3
  38. package/src/server/management/model-rows.ts +18 -3
  39. package/src/server/management/provider-routes.ts +107 -3
  40. package/src/server/management-auth.ts +65 -1
  41. package/src/server/proxy-liveness.ts +1 -0
  42. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  43. package/src/server/responses/agent-task-recovery.ts +460 -0
  44. package/src/server/responses/compact.ts +4 -2
  45. package/src/server/responses/core.ts +142 -6
  46. package/src/server/responses/encrypted-payload.ts +4 -1
  47. package/src/server/search.ts +4 -0
  48. package/src/types.ts +27 -0
  49. package/src/usage/expected-prices.ts +11 -0
  50. package/src/vision/describe.ts +4 -0
  51. package/src/web-search/anthropic-executor.ts +5 -1
  52. package/src/web-search/executor.ts +9 -1
  53. package/src/web-search/index.ts +5 -0
  54. package/src/web-search/loop.ts +42 -5
  55. package/gui/dist/assets/index-BHldBl6_.js +0 -76
@@ -19,6 +19,12 @@
19
19
  * - `GET /v1/live/{callId}` — Frameless
20
20
  * - `GET /v1/realtime/calls/{callId}` — path-form join
21
21
  * - `GET /v1/realtime?call_id=` — Realtime v1/v2 join
22
+ *
23
+ * Inbound standalone session WebSocket (no call-create; codex-rs `thread/realtime/start`
24
+ * with the standalone WebSocket transport — the desktop voice path since 0.147.x):
25
+ * - `GET /v1/realtime?intent=quicksilver&model=` — Realtime v1 standalone
26
+ * - `GET /v1/realtime?model=` — RealtimeV2 standalone (no intent)
27
+ * - `GET /v1/live?model=` — Frameless standalone
22
28
  */
23
29
  import { appendFileSync } from "node:fs";
24
30
  import { formatErrorResponse } from "../bridge";
@@ -32,7 +38,7 @@ import {
32
38
  CodexThreadAffinityExpiredError,
33
39
  } from "../codex/auth-context";
34
40
  import { formatCodexProviderForLog } from "../codex/routing";
35
- import { signalWithTimeout } from "../lib/abort";
41
+ import { cancelBodyOnAbort, signalWithTimeout } from "../lib/abort";
36
42
  import { sidecarEnter } from "../lib/sidecar-tracker";
37
43
  import type { OcxConfig } from "../types";
38
44
  import { resolveFirstUsableOpenAiSidecar, selectOpenAiImagesProvider } from "../providers/openai-sidecar";
@@ -141,10 +147,59 @@ function clientProtocolHeaders(reqHeaders: Headers): Record<string, string> {
141
147
 
142
148
  const LIVE_CALL_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
143
149
 
150
+ /**
151
+ * Credential-shaped query keys never forwarded upstream on a standalone realtime
152
+ * relay. Auth on the upstream socket is proxy-owned (headers resolved by
153
+ * `resolveLiveRelay`); a caller that puts `access_token=`/`api_key=`/... in the
154
+ * URL must not get it relayed to the configured upstream. Compared case-folded on
155
+ * both the raw and percent-decoded key. Everything else — `intent`, `model`,
156
+ * duplicates, protocol extensions — passes through verbatim, matching codex-rs
157
+ * client behavior of constructing those fields itself.
158
+ */
159
+ const STANDALONE_QUERY_DENYLIST = new Set([
160
+ "access_token",
161
+ "api_key",
162
+ "apikey",
163
+ "token",
164
+ "key",
165
+ "authorization",
166
+ "auth",
167
+ "signature",
168
+ "sig",
169
+ ]);
170
+
171
+ /**
172
+ * Filter a raw query string (no leading `?`) for standalone upstream relay: drop
173
+ * denylisted credential-shaped pairs, preserve the rest byte-for-byte (including
174
+ * ordering, duplicates, noncanonical encodings, and bare keys).
175
+ */
176
+ export function sanitizeStandaloneRealtimeQuery(rawQuery: string): string {
177
+ if (!rawQuery) return "";
178
+ const kept: string[] = [];
179
+ for (const pair of rawQuery.split("&")) {
180
+ const eq = pair.indexOf("=");
181
+ const rawKey = eq === -1 ? pair : pair.slice(0, eq);
182
+ let decodedKey = rawKey;
183
+ try {
184
+ decodedKey = decodeURIComponent(rawKey);
185
+ } catch {
186
+ // Leave undecodable keys as-is; the raw comparison still applies.
187
+ }
188
+ if (STANDALONE_QUERY_DENYLIST.has(rawKey.toLowerCase()) || STANDALONE_QUERY_DENYLIST.has(decodedKey.toLowerCase())) {
189
+ console.warn(`[live] standalone realtime relay dropping credential-shaped query param: ${decodedKey}`);
190
+ continue;
191
+ }
192
+ kept.push(pair);
193
+ }
194
+ return kept.join("&");
195
+ }
196
+
144
197
  export type LiveSidebandTarget =
145
198
  | { style: "frameless-path"; callId: string }
146
199
  | { style: "realtime-calls-path"; callId: string }
147
- | { style: "realtime-query"; callId: string };
200
+ | { style: "realtime-query"; callId: string }
201
+ | { style: "realtime-standalone"; query: string }
202
+ | { style: "frameless-standalone"; query: string };
148
203
 
149
204
  export type LiveRelayTarget = {
150
205
  headers: Record<string, string>;
@@ -168,7 +223,7 @@ export function keyedLiveUrl(baseUrl: string): string {
168
223
  }
169
224
 
170
225
  export function forwardLiveUrl(baseUrl: string, usesBackendShape: boolean): string {
171
- const root = baseUrl.replace(/\/$/, "");
226
+ const root = baseUrl.replace(/\/+$/, "");
172
227
  if (usesBackendShape) return withAvasQuery(`${root}/realtime/calls`);
173
228
  // Frameless API shape posts to /live without the AVAS query (codex RealtimeCallClient).
174
229
  return `${root}/live`;
@@ -180,13 +235,17 @@ function httpsToWss(httpUrl: string): string {
180
235
  return httpUrl;
181
236
  }
182
237
 
183
- export function parseLiveSidebandTarget(pathname: string, searchParams: URLSearchParams): LiveSidebandTarget | null {
238
+ export function parseLiveSidebandTarget(pathname: string, searchParams: URLSearchParams, rawQuery = ""): LiveSidebandTarget | null {
184
239
  const liveMatch = pathname.match(/^\/v1\/live\/([^/]+)\/?$/);
185
240
  if (liveMatch) {
186
241
  const callId = decodeURIComponent(liveMatch[1]!);
187
242
  if (!LIVE_CALL_ID_RE.test(callId)) return null;
188
243
  return { style: "frameless-path", callId };
189
244
  }
245
+ // Standalone Frameless session (no call-create): `GET /v1/live?model=`.
246
+ if (pathname === "/v1/live" || pathname === "/v1/live/") {
247
+ return { style: "frameless-standalone", query: sanitizeStandaloneRealtimeQuery(rawQuery) };
248
+ }
190
249
  const callsMatch = pathname.match(/^\/v1\/realtime\/calls\/([^/]+)\/?$/);
191
250
  if (callsMatch) {
192
251
  const callId = decodeURIComponent(callsMatch[1]!);
@@ -194,9 +253,17 @@ export function parseLiveSidebandTarget(pathname: string, searchParams: URLSearc
194
253
  return { style: "realtime-calls-path", callId };
195
254
  }
196
255
  if (pathname === "/v1/realtime" || pathname === "/v1/realtime/") {
197
- const callId = searchParams.get("call_id")?.trim() ?? "";
198
- if (!LIVE_CALL_ID_RE.test(callId)) return null;
199
- return { style: "realtime-query", callId };
256
+ // A present-but-invalid `call_id` is a malformed join, not a standalone
257
+ // session keep rejecting it instead of silently changing the request's
258
+ // meaning.
259
+ if (searchParams.has("call_id")) {
260
+ const callId = searchParams.get("call_id")?.trim() ?? "";
261
+ if (!LIVE_CALL_ID_RE.test(callId)) return null;
262
+ return { style: "realtime-query", callId };
263
+ }
264
+ // Standalone Realtime session (codex-rs thread/realtime/start, WebSocket
265
+ // transport): v1 sends `intent=quicksilver&model=`, v2 sends `model=` only.
266
+ return { style: "realtime-standalone", query: sanitizeStandaloneRealtimeQuery(rawQuery) };
200
267
  }
201
268
  return null;
202
269
  }
@@ -286,6 +353,12 @@ export function buildLiveSidebandUpstreamWsUrl(
286
353
  if (target.style === "frameless-path") {
287
354
  return httpsToWss(`${sidebandRoot}/live/${target.callId}`);
288
355
  }
356
+ if (target.style === "frameless-standalone") {
357
+ return httpsToWss(`${sidebandRoot}/live${target.query ? `?${target.query}` : ""}`);
358
+ }
359
+ if (target.style === "realtime-standalone") {
360
+ return httpsToWss(`${sidebandRoot}/realtime${target.query ? `?${target.query}` : ""}`);
361
+ }
289
362
  if (target.style === "realtime-calls-path") {
290
363
  return httpsToWss(`${sidebandRoot}/realtime/calls/${target.callId}`);
291
364
  }
@@ -364,8 +437,18 @@ export async function readBodyCapped(
364
437
  }
365
438
  chunks.push(value);
366
439
  }
440
+ } catch (err) {
441
+ // A read that throws leaves the stream neither drained nor cancelled, and releasing the
442
+ // lock alone hands back an unsettled body. Cancel first, then rethrow so the caller's
443
+ // existing classification (client abort / timeout / connect error) is unchanged. The
444
+ // cancel itself can reject with the stream's stored error — that is expected and must not
445
+ // mask the original failure, so it is swallowed here.
446
+ await reader.cancel(err).catch(() => {});
447
+ throw err;
367
448
  } finally {
368
449
  try {
450
+ // Always release: `reader.cancel()` does NOT drop the lock, and holding it would leave
451
+ // the stream permanently locked for any later consumer (audit R-WP5-2).
369
452
  reader.releaseLock();
370
453
  } catch {
371
454
  // already released / cancelled
@@ -546,7 +629,12 @@ export async function handleLive(
546
629
  outboundContentType = rewritten.contentType;
547
630
  }
548
631
  } else {
549
- url = keyedLiveUrl(relay.providerBaseUrl);
632
+ // Frameless API-shape call-create posts to `{base}/live` without the AVAS
633
+ // query (openai/codex RealtimeCallClient, realtime_call.rs); only the
634
+ // realtime/calls inbound shape keeps the legacy keyed AVAS endpoint.
635
+ url = new URL(req.url).pathname === "/v1/live"
636
+ ? forwardLiveUrl(relay.providerBaseUrl, /* usesBackendShape */ false)
637
+ : keyedLiveUrl(relay.providerBaseUrl);
550
638
  }
551
639
 
552
640
  headers["content-type"] = outboundContentType;
@@ -559,15 +647,31 @@ export async function handleLive(
559
647
  headers,
560
648
  body: outboundBody,
561
649
  signal: linkedSignal.signal,
650
+ // Credential-bearing: do not follow a cross-origin 3xx. Bun strips `Authorization`
651
+ // across origins but forwards nonstandard headers such as `chatgpt-account-id`,
652
+ // `session_id`, and `x-codex-turn-metadata` to the redirect target.
653
+ redirect: "manual",
562
654
  });
563
655
  // Record every completed upstream response before body size handling so account health /
564
656
  // cooldown still updates when we reject an oversized payload.
565
657
  relay.recordOutcome?.(upstreamResponse.status);
566
- const payload = await readBodyCapped(
567
- upstreamResponse.body,
568
- LIVE_RESPONSE_MAX_BYTES,
569
- total => `live response too large (${total} bytes)`,
570
- );
658
+ // Settle the body on abort before the reader attaches. Without this, a client cancel or the
659
+ // linked timeout landing between fetch resolution and `readBodyCapped`'s `getReader()`
660
+ // leaves Bun's internal read rejection orphaned off the awaited path, where no caller
661
+ // try/catch can intercept it (src/lib/abort.ts). The guard covers the window BEFORE the
662
+ // reader exists; once a reader holds the lock only the reader can cancel, which is why
663
+ // readBodyCapped also cancels on a failed read. Found while investigating #1419.
664
+ const detachBodyGuard = cancelBodyOnAbort(upstreamResponse.body, linkedSignal.signal);
665
+ let payload: ArrayBuffer | Response;
666
+ try {
667
+ payload = await readBodyCapped(
668
+ upstreamResponse.body,
669
+ LIVE_RESPONSE_MAX_BYTES,
670
+ total => `live response too large (${total} bytes)`,
671
+ );
672
+ } finally {
673
+ detachBodyGuard();
674
+ }
571
675
  if (payload instanceof Response) return payload;
572
676
  const relayHeaders: Record<string, string> = {};
573
677
  for (const name of LIVE_RELAY_HEADERS) {
@@ -0,0 +1,137 @@
1
+ import { readRuntimePort, type RuntimePortState } from "../config";
2
+ import {
3
+ LOCAL_ATTESTATION_CHALLENGE_HEADER,
4
+ LOCAL_ATTESTATION_PROOF_HEADER,
5
+ createLocalAttestationChallenge,
6
+ verifyLocalAttestationProof,
7
+ } from "../lib/local-management-attestation";
8
+ import {
9
+ LOCAL_PROVIDER_RELOAD_CAPABILITY_HEADER,
10
+ LOCAL_PROVIDER_RELOAD_CAPABILITY_TTL_MS,
11
+ LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION,
12
+ LOCAL_PROVIDER_RELOAD_EXPECTED_PID_HEADER,
13
+ LOCAL_PROVIDER_RELOAD_EXPIRES_AT_HEADER,
14
+ LOCAL_PROVIDER_RELOAD_METHOD,
15
+ LOCAL_PROVIDER_RELOAD_NAME_HEADER,
16
+ LOCAL_PROVIDER_RELOAD_NONCE_HEADER,
17
+ LOCAL_PROVIDER_RELOAD_PATH,
18
+ createLocalProviderReloadCapability,
19
+ isLocalProviderReloadName,
20
+ } from "../lib/local-provider-reload-contract";
21
+ import { directLocalHttpFetch } from "./direct-local-http";
22
+ import { isOpencodexHealthz, probeHostname, type HealthzIdentity, type LiveProxy } from "./proxy-liveness";
23
+
24
+ export type LocalProviderReloadResult =
25
+ | { kind: "reloaded" }
26
+ | { kind: "unavailable"; reason: "invalid-name" | "unattested-target" | "runtime-mismatch" | "attestation" | "capability" | "transport" | "rejected" };
27
+
28
+ export interface LocalProviderReloadDeps {
29
+ fetchImpl?: typeof fetch;
30
+ readRuntime?: (pid: number) => RuntimePortState | null;
31
+ createNonce?: () => string;
32
+ now?: () => number;
33
+ timeoutMs?: number;
34
+ }
35
+
36
+ const LOCAL_PROVIDER_RELOAD_TIMEOUT_MS = 10_000;
37
+
38
+ /**
39
+ * Ask the exact runtime proxy to reload one already-persisted provider.
40
+ *
41
+ * No provider object, API key, OAuth token, custom header, or reusable management
42
+ * credential crosses the socket. The request is bodyless and its one-shot capability
43
+ * binds the provider name to the attested process, method, path, PID, port, and expiry.
44
+ */
45
+ export async function requestBoundLocalProviderReload(
46
+ target: LiveProxy,
47
+ name: string,
48
+ deps: LocalProviderReloadDeps = {},
49
+ ): Promise<LocalProviderReloadResult> {
50
+ if (!isLocalProviderReloadName(name)) return { kind: "unavailable", reason: "invalid-name" };
51
+ if (target.source !== "runtime" || target.pid === null || target.pid <= 0) {
52
+ return { kind: "unavailable", reason: "unattested-target" };
53
+ }
54
+ const readRuntime = deps.readRuntime ?? readRuntimePort;
55
+ const runtime = readRuntime(target.pid);
56
+ if (
57
+ !runtime?.attestationSecret
58
+ || runtime.pid !== target.pid
59
+ || runtime.port !== target.port
60
+ ) {
61
+ return { kind: "unavailable", reason: "runtime-mismatch" };
62
+ }
63
+
64
+ const fetchImpl = deps.fetchImpl ?? directLocalHttpFetch;
65
+ const timeoutMs = deps.timeoutMs ?? LOCAL_PROVIDER_RELOAD_TIMEOUT_MS;
66
+ const nonce = (deps.createNonce ?? createLocalAttestationChallenge)();
67
+ const baseUrl = `http://${probeHostname(target.hostname)}:${target.port}`;
68
+ let proofResponse: Response;
69
+ try {
70
+ proofResponse = await fetchImpl(`${baseUrl}/healthz`, {
71
+ headers: { [LOCAL_ATTESTATION_CHALLENGE_HEADER]: nonce },
72
+ signal: AbortSignal.timeout(timeoutMs),
73
+ });
74
+ } catch {
75
+ return { kind: "unavailable", reason: "transport" };
76
+ }
77
+ const body = await proofResponse.json().catch(() => null) as HealthzIdentity | null;
78
+ if (
79
+ !proofResponse.ok
80
+ || !isOpencodexHealthz(body)
81
+ || body?.pid !== target.pid
82
+ || body?.port !== target.port
83
+ || body?.providerReloadCapability !== LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION
84
+ || !verifyLocalAttestationProof(
85
+ runtime.attestationSecret,
86
+ nonce,
87
+ target.pid,
88
+ target.port,
89
+ proofResponse.headers.get(LOCAL_ATTESTATION_PROOF_HEADER),
90
+ )
91
+ ) {
92
+ return { kind: "unavailable", reason: "attestation" };
93
+ }
94
+
95
+ const currentRuntime = readRuntime(target.pid);
96
+ if (
97
+ !currentRuntime?.attestationSecret
98
+ || currentRuntime.pid !== runtime.pid
99
+ || currentRuntime.port !== runtime.port
100
+ || currentRuntime.hostname !== runtime.hostname
101
+ || currentRuntime.attestationSecret !== runtime.attestationSecret
102
+ ) {
103
+ return { kind: "unavailable", reason: "runtime-mismatch" };
104
+ }
105
+
106
+ const expiresAt = (deps.now ?? Date.now)() + LOCAL_PROVIDER_RELOAD_CAPABILITY_TTL_MS;
107
+ const capability = createLocalProviderReloadCapability(
108
+ runtime.attestationSecret,
109
+ nonce,
110
+ LOCAL_PROVIDER_RELOAD_METHOD,
111
+ LOCAL_PROVIDER_RELOAD_PATH,
112
+ name,
113
+ target.pid,
114
+ target.port,
115
+ expiresAt,
116
+ );
117
+ if (!capability) return { kind: "unavailable", reason: "capability" };
118
+
119
+ try {
120
+ const response = await fetchImpl(`${baseUrl}${LOCAL_PROVIDER_RELOAD_PATH}`, {
121
+ method: LOCAL_PROVIDER_RELOAD_METHOD,
122
+ headers: {
123
+ [LOCAL_PROVIDER_RELOAD_EXPECTED_PID_HEADER]: String(target.pid),
124
+ [LOCAL_PROVIDER_RELOAD_NONCE_HEADER]: nonce,
125
+ [LOCAL_PROVIDER_RELOAD_EXPIRES_AT_HEADER]: String(expiresAt),
126
+ [LOCAL_PROVIDER_RELOAD_NAME_HEADER]: name,
127
+ [LOCAL_PROVIDER_RELOAD_CAPABILITY_HEADER]: capability,
128
+ },
129
+ signal: AbortSignal.timeout(timeoutMs),
130
+ });
131
+ return response.ok
132
+ ? { kind: "reloaded" }
133
+ : { kind: "unavailable", reason: "rejected" };
134
+ } catch {
135
+ return { kind: "unavailable", reason: "transport" };
136
+ }
137
+ }
@@ -410,7 +410,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
410
410
  const vs = config.visionSidecar ?? {};
411
411
  const vision = await sidecarVisionResponseSettings(config);
412
412
  return jsonResponse({
413
- webSearch: { model: ws.model ?? "gpt-5.6-luna", backend: ws.backend },
413
+ webSearch: {
414
+ model: ws.model ?? "gpt-5.6-luna",
415
+ backend: ws.backend,
416
+ streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
417
+ },
414
418
  vision: {
415
419
  model: vision.model,
416
420
  backend: vs.backend,
@@ -430,13 +434,17 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
430
434
  if (raw.webSearch !== undefined && !isPlainRecord(raw.webSearch)) return jsonResponse({ error: "webSearch must be an object" }, 400);
431
435
  if (raw.vision !== undefined && !isPlainRecord(raw.vision)) return jsonResponse({ error: "vision must be an object" }, 400);
432
436
  const body = raw as {
433
- webSearch?: { model?: unknown; backend?: unknown; reasoning?: unknown };
437
+ webSearch?: { model?: unknown; backend?: unknown; reasoning?: unknown; streamRoutedModelOutput?: unknown };
434
438
  vision?: { model?: unknown; backend?: unknown; reasoning?: unknown; maxDescriptionsPerTurn?: unknown };
435
439
  };
436
440
  if (body.webSearch && body.webSearch.backend !== undefined && body.webSearch.backend !== null
437
441
  && body.webSearch.backend !== "openai" && body.webSearch.backend !== "anthropic") {
438
442
  return jsonResponse({ error: "webSearch.backend must be openai, anthropic, or null" }, 400);
439
443
  }
444
+ if (body.webSearch && body.webSearch.streamRoutedModelOutput !== undefined
445
+ && typeof body.webSearch.streamRoutedModelOutput !== "boolean") {
446
+ return jsonResponse({ error: "webSearch.streamRoutedModelOutput must be a boolean" }, 400);
447
+ }
440
448
  if (body.vision && body.vision.backend !== undefined
441
449
  && body.vision.backend !== null && body.vision.backend !== "openai" && body.vision.backend !== "anthropic") {
442
450
  return jsonResponse({ error: "vision.backend must be openai, anthropic, or null" }, 400);
@@ -490,6 +498,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
490
498
  config.webSearchSidecar.backend = body.webSearch.backend;
491
499
  }
492
500
  if (typeof body.webSearch.reasoning === "string") config.webSearchSidecar.reasoning = body.webSearch.reasoning;
501
+ if (typeof body.webSearch.streamRoutedModelOutput === "boolean") {
502
+ // `false` is the default — drop the key so config files stay minimal.
503
+ if (body.webSearch.streamRoutedModelOutput) config.webSearchSidecar.streamRoutedModelOutput = true;
504
+ else delete config.webSearchSidecar.streamRoutedModelOutput;
505
+ }
493
506
  }
494
507
  if (body.vision) {
495
508
  config.visionSidecar = { ...config.visionSidecar };
@@ -515,7 +528,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
515
528
  const vision = await sidecarVisionResponseSettings(config);
516
529
  return jsonResponse({
517
530
  ok: true,
518
- webSearch: { model: ws.model ?? "gpt-5.6-luna", backend: ws.backend },
531
+ webSearch: {
532
+ model: ws.model ?? "gpt-5.6-luna",
533
+ backend: ws.backend,
534
+ streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
535
+ },
519
536
  vision: {
520
537
  model: vision.model,
521
538
  backend: vs.backend,
@@ -121,6 +121,29 @@ function refreshedUsageSummary<T extends UsageSummary & { historyTruncated: bool
121
121
  return { ...summary, since, generatedAt: now };
122
122
  }
123
123
 
124
+ /**
125
+ * Timestamp bounds of the rows the bounded reader actually loaded.
126
+ *
127
+ * Deliberately computed over the whole snapshot, BEFORE `summarizeUsage` applies the range
128
+ * and surface predicates: truncation is a property of the read, not of the query, so the
129
+ * window that matters to a client is the one the reader could see. It is not a completeness
130
+ * claim and must never be presented as one. `usage.jsonl` is appended when a request
131
+ * COMPLETES while each row carries the request START time, so a long-running request can be
132
+ * appended after shorter ones that started later — meaning the oldest loaded timestamp does
133
+ * not bound what the dropped prefix contains (#1497).
134
+ */
135
+ function snapshotWindow(entries: PersistedUsageEntry[]): { start: number | null; end: number | null } {
136
+ let start: number | null = null;
137
+ let end: number | null = null;
138
+ for (const entry of entries) {
139
+ const at = entry.timestamp;
140
+ if (typeof at !== "number" || !Number.isFinite(at)) continue;
141
+ if (start === null || at < start) start = at;
142
+ if (end === null || at > end) end = at;
143
+ }
144
+ return { start, end };
145
+ }
146
+
124
147
  export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Response | null> {
125
148
  const { req, url, config, deps, syncClaudeAgentDefsBestEffort } = ctx;
126
149
 
@@ -209,12 +232,15 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
209
232
  const overlayVersion = userCostOverlayVersion();
210
233
  const snapshot = await readUsageSnapshotForManagement(effectiveReadLimit);
211
234
  const revisionReadAt = Date.now();
235
+ const window = snapshotWindow(snapshot.entries);
212
236
  const summary = {
213
237
  ...summarizeUsage(snapshot.entries, range, now, surface),
214
238
  historyTruncated: snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated,
215
239
  truncatedPrefixBytes: snapshot.truncatedPrefixBytes,
216
240
  entriesTruncated: snapshot.entriesTruncated,
217
241
  entriesDropped: snapshot.entriesDropped,
242
+ snapshotWindowStart: window.start,
243
+ snapshotWindowEnd: window.end,
218
244
  };
219
245
  if (userCostOverlayVersion() !== overlayVersion) {
220
246
  // The overlay changed while the summary was being computed, so this
@@ -266,6 +292,8 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
266
292
  truncatedPrefixBytes: 0,
267
293
  entriesTruncated: false,
268
294
  entriesDropped: 0,
295
+ snapshotWindowStart: null,
296
+ snapshotWindowEnd: null,
269
297
  error: "read_failed",
270
298
  });
271
299
  }
@@ -30,7 +30,7 @@ function readInputModalities(raw: unknown): { values?: string[]; error?: string
30
30
  return { values: raw as string[] };
31
31
  }
32
32
  import type { CatalogModel } from "../../codex/catalog";
33
- import { catalogModelSlug, configuredNativeAliasSlugs, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
33
+ import { accountBoundNativeOpenAiSlugsBySelector, catalogModelSlug, configuredNativeAliasSlugs, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, shouldIncludeAccountBoundNativeOpenAi, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
34
34
  import { CatalogGatherBusyError } from "../../codex/catalog/provider-fetch";
35
35
  import { getProviderLiveModelCount } from "../../codex/model-cache";
36
36
  import {
@@ -234,7 +234,14 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
234
234
  if (!providerConfig && provider !== "openai" && !isVirtualComboNamespace) {
235
235
  return jsonResponse({ error: "unknown model visibility provider" }, 400);
236
236
  }
237
- const supportedNative = new Set(nativeModelRows(config).map(row => row.slug));
237
+ const accountNativeQualified = shouldIncludeAccountBoundNativeOpenAi(config)
238
+ ? [...accountBoundNativeOpenAiSlugsBySelector(config).entries()].flatMap(([selector, slugs]) =>
239
+ slugs.filter(slug => !nativeModelRows(config).some(row => row.slug === slug)).map(slug => `${selector}/${slug}`))
240
+ : [];
241
+ const supportedNative = new Set([
242
+ ...nativeModelRows(config).map(row => row.slug),
243
+ ...accountNativeQualified,
244
+ ]);
238
245
  const targets: Array<{ id: string; native: boolean }> = [];
239
246
  const seen = new Set<string>();
240
247
  for (const value of body.targets) {
@@ -283,13 +290,14 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
283
290
  const nativeIds = provider === "openai"
284
291
  ? disabledNativeSlugs({ disabledModels: disabled })
285
292
  : new Set<string>();
293
+ const accountNativeIds = provider === "openai" ? new Set(accountNativeQualified) : new Set<string>();
286
294
  const nativeAliasSlugs = provider === "openai"
287
295
  ? configuredNativeAliasSlugs(config)
288
296
  : new Set<string>();
289
297
  disabled = disabled.filter(stored => (
290
298
  knownComboSelectors.has(stored)
291
299
  || nativeAliasSlugs.has(stored)
292
- || (!stored.startsWith(`${provider}/`) && !nativeIds.has(stored))
300
+ || (!stored.startsWith(`${provider}/`) && !nativeIds.has(stored) && !accountNativeIds.has(stored))
293
301
  ));
294
302
  }
295
303
  } else {
@@ -11,11 +11,14 @@
11
11
  import type { CatalogModel } from "../../codex/catalog";
12
12
  import {
13
13
  catalogModelSlug,
14
+ accountBoundNativeOpenAiSlugsBySelector,
14
15
  nativeDefaultReasoningEffort,
16
+ NATIVE_OPENAI_MODELS,
15
17
  nativeInputModalities,
16
18
  nativeModelRows,
17
19
  nativeReasoningEfforts,
18
20
  uniqueCatalogModelsForPublicList,
21
+ shouldIncludeAccountBoundNativeOpenAi,
19
22
  } from "../../codex/catalog";
20
23
  import type { ExportModel } from "../../clients/config-export";
21
24
  import { providerContextCap } from "../../providers/context-cap";
@@ -49,9 +52,21 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
49
52
  const disabled = new Set(config.disabledModels ?? []);
50
53
  // Native GPT passthrough rows lead (provider "openai", bare-slug namespaced ids): sourced
51
54
  // from the static supported set so a disabled model stays listed and re-enableable.
52
- const native: ManagementModelRow[] = nativeModelRows(config).map(row => {
53
- const reasoningEfforts = nativeReasoningEfforts(row.slug).filter(isVisionReasoningEffort);
54
- const defaultReasoningEffort = nativeDefaultReasoningEffort(row.slug);
55
+ const nativeRows = nativeModelRows(config).map(row => ({ ...row, metadataSlug: row.slug }));
56
+ const accountNativeRows = shouldIncludeAccountBoundNativeOpenAi(config)
57
+ ? [...accountBoundNativeOpenAiSlugsBySelector(config).entries()].flatMap(([selector, slugs]) =>
58
+ slugs
59
+ .filter(slug => !NATIVE_OPENAI_MODELS.includes(slug))
60
+ .map(slug => ({
61
+ slug: `${selector}/${slug}`,
62
+ metadataSlug: slug,
63
+ disabled: disabled.has(`${selector}/${slug}`) || disabled.has(slug),
64
+ contextWindow: undefined,
65
+ })))
66
+ : [];
67
+ const native: ManagementModelRow[] = [...nativeRows, ...accountNativeRows].map(row => {
68
+ const reasoningEfforts = nativeReasoningEfforts(row.metadataSlug).filter(isVisionReasoningEffort);
69
+ const defaultReasoningEffort = nativeDefaultReasoningEffort(row.metadataSlug);
55
70
  return {
56
71
  provider: "openai",
57
72
  id: row.slug,