@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.
- package/gui/dist/assets/index-Co12XTT-.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +5 -1
- package/src/adapters/cursor/request-builder.ts +3 -3
- package/src/adapters/google.ts +25 -5
- package/src/adapters/openai-chat.ts +182 -6
- package/src/adapters/openai-responses.ts +17 -7
- package/src/codex/catalog/bundled.ts +16 -0
- package/src/codex/catalog/metadata.ts +180 -5
- package/src/codex/catalog/parsing.ts +7 -6
- package/src/codex/catalog/sync.ts +73 -6
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence.ts +20 -0
- package/src/codex/prompt-journal.ts +50 -13
- package/src/codex/prompt-layers.ts +1 -1
- package/src/config.ts +57 -0
- package/src/generated/compatibility-version.json +64 -48
- package/src/generated/model-metadata.ts +3 -3
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/oauth/login-cli.ts +52 -26
- package/src/providers/derive.ts +2 -0
- package/src/providers/openai-sidecar.ts +9 -2
- package/src/providers/quota.ts +57 -0
- package/src/providers/registry.ts +53 -25
- package/src/responses/state.ts +22 -0
- package/src/router.ts +1 -0
- package/src/server/claude-messages.ts +57 -11
- package/src/server/direct-local-http.ts +7 -3
- package/src/server/images.ts +6 -0
- package/src/server/index.ts +51 -11
- package/src/server/live.ts +117 -13
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/config-routes.ts +20 -3
- package/src/server/management/logs-usage-routes.ts +28 -0
- package/src/server/management/model-routes.ts +11 -3
- package/src/server/management/model-rows.ts +18 -3
- package/src/server/management/provider-routes.ts +107 -3
- package/src/server/management-auth.ts +65 -1
- package/src/server/proxy-liveness.ts +1 -0
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +460 -0
- package/src/server/responses/compact.ts +4 -2
- package/src/server/responses/core.ts +142 -6
- package/src/server/responses/encrypted-payload.ts +4 -1
- package/src/server/search.ts +4 -0
- package/src/types.ts +27 -0
- package/src/usage/expected-prices.ts +11 -0
- package/src/vision/describe.ts +4 -0
- package/src/web-search/anthropic-executor.ts +5 -1
- package/src/web-search/executor.ts +9 -1
- package/src/web-search/index.ts +5 -0
- package/src/web-search/loop.ts +42 -5
- package/gui/dist/assets/index-BHldBl6_.js +0 -76
package/src/server/live.ts
CHANGED
|
@@ -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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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: {
|
|
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: {
|
|
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
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
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,
|