@bitkyc08/opencodex 2.87.0 → 2.89.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/README.md +11 -1
- package/bin/ocx.mjs +15 -4
- package/gui/dist/assets/App-BnRSujOq.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-DCd-A1az.js → Tray-7z1COnmz.js} +1 -1
- package/gui/dist/assets/index-uchD-ngf.js +94 -0
- package/gui/dist/assets/{quota-summary-ChvmKGOk.js → quota-summary-BY_10IEz.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +86 -48
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +52 -30
- package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/passthrough.ts +17 -12
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/registry.ts +1 -1
- package/src/adapters/upstream-http-error.ts +3 -2
- package/src/adapters/zed.ts +7 -4
- package/src/bridge/internal.ts +17 -3
- package/src/bridge/response-json.ts +4 -4
- package/src/bridge/sse.ts +17 -10
- package/src/claude/inbound-content-options.ts +66 -0
- package/src/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +18 -5
- package/src/cli/account-api.ts +13 -1
- package/src/cli/account-extended.ts +17 -5
- package/src/cli/account-key-quota.ts +4 -1
- package/src/cli/account.ts +1 -1
- package/src/cli/capabilities-agents-routing.ts +45 -8
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-provider-models.ts +5 -4
- package/src/cli/combo-result.ts +8 -2
- package/src/cli/combo.ts +17 -0
- package/src/cli/cross-home-owner.ts +16 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/loopback-family.ts +31 -0
- package/src/cli/message-command.ts +4 -1
- package/src/cli/message-remote-args.ts +69 -0
- package/src/cli/message-remote-command.ts +77 -0
- package/src/cli/provider-runtime.ts +69 -1
- package/src/cli/registry.ts +3 -3
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +30 -3
- package/src/codex/auth-api/account-list.ts +3 -0
- package/src/codex/auth-api/main-account-probe.ts +3 -2
- package/src/codex/auth-context.ts +9 -4
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/auto-review.ts +2 -1
- package/src/codex/catalog/build-entries.ts +17 -7
- package/src/codex/catalog/derive-entry.ts +3 -1
- package/src/codex/catalog/metadata.ts +2 -0
- package/src/codex/catalog/model-visibility.ts +0 -2
- package/src/codex/catalog/ownership.ts +71 -0
- package/src/codex/catalog/parsing.ts +12 -3
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/catalog/routed-gather.ts +3 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- package/src/codex/convergence.ts +2 -1
- package/src/codex/low-quota-desktop-notifier.ts +125 -0
- package/src/codex/low-quota-protection.ts +57 -31
- package/src/codex/main-account-cache.ts +77 -3
- package/src/codex/main-account.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- package/src/codex/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- package/src/combos/failover.ts +2 -1
- package/src/combos/jev-model-backend.ts +30 -7
- package/src/combos/jev-model-request.ts +20 -0
- package/src/combos/jev-quota-activation.ts +153 -0
- package/src/combos/jev-quota-config.ts +62 -0
- package/src/combos/jev-quota-route.ts +26 -0
- package/src/combos/jev-quota.ts +88 -0
- package/src/combos/jev.ts +25 -12
- package/src/combos/types.ts +27 -0
- package/src/compatibility/index.ts +3 -0
- package/src/compatibility/openai-responses.ts +8 -1
- package/src/compatibility/opengateway-responses.ts +61 -0
- package/src/config/account-auto-pause-revisions.ts +136 -0
- package/src/config/diagnostics.ts +13 -0
- package/src/config/live-reconcile.ts +9 -0
- package/src/config/opencode-zen-upgrade.ts +9 -8
- package/src/config/persist-unlocked.ts +10 -1
- package/src/config/removed-opencode-free.ts +135 -0
- package/src/config/schema/account-auto-pause.ts +14 -0
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +7 -0
- package/src/config.ts +10 -3
- package/src/generated/compatibility-version.json +411 -175
- package/src/images/loop.ts +101 -27
- package/src/lib/account-auto-pause-response.ts +16 -0
- package/src/lib/bun-runtime.ts +18 -1
- package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/request-failure-model.ts +2 -0
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/link/ssh-argv.ts +3 -0
- package/src/messaging/envelope.ts +6 -3
- package/src/messaging/remote-auth.ts +83 -0
- package/src/messaging/remote-bridge.ts +270 -0
- package/src/messaging/remote-contract.ts +108 -0
- package/src/messaging/remote-enrollment-recovery.ts +59 -0
- package/src/messaging/remote-enrollment.ts +244 -0
- package/src/messaging/remote-envelope.ts +40 -0
- package/src/messaging/remote-files.ts +117 -0
- package/src/messaging/remote-owner.ts +169 -0
- package/src/messaging/remote-ports.ts +56 -0
- package/src/messaging/remote-process.ts +131 -0
- package/src/messaging/remote-replies.ts +98 -0
- package/src/messaging/remote-rpc-admission.ts +32 -0
- package/src/messaging/remote-send.ts +72 -0
- package/src/messaging/remote-store.ts +111 -0
- package/src/messaging/remote-tunnels.ts +65 -0
- package/src/messaging/rpc.ts +13 -1
- package/src/oauth/account-auto-pause-policy.ts +66 -0
- package/src/oauth/account-auto-pause.ts +171 -0
- package/src/oauth/account-quota-rank.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +6 -0
- package/src/oauth/anthropic-model-family.ts +8 -0
- package/src/oauth/anthropic-model-quota.ts +2 -6
- package/src/oauth/api-key-pool-preserve.ts +37 -0
- package/src/oauth/generic-account-failover.ts +7 -4
- package/src/oauth/index.ts +5 -35
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/protocols/encoders/adapter-events.ts +5 -5
- package/src/protocols/encoders/messages.ts +2 -2
- package/src/providers/api-key-pause.ts +32 -0
- package/src/providers/api-key-resolve.ts +10 -4
- package/src/providers/api-key-selection.ts +6 -1
- package/src/providers/api-keys.ts +8 -4
- package/src/providers/claude-cli-usage.ts +5 -0
- package/src/providers/key-failover.ts +11 -11
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +12 -73
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +2 -1
- package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
- package/src/providers/quota/anthropic-decision-windows.ts +47 -0
- package/src/providers/quota/antigravity.ts +39 -6
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota-decision-publication.ts +77 -0
- package/src/providers/quota-decision-snapshot.ts +145 -0
- package/src/providers/quota-routing-cache.ts +3 -0
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +3 -2
- package/src/providers/registry/entries-extended.ts +8 -52
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/responses/parser.ts +18 -10
- package/src/responses/reasoning-replay-cache.ts +50 -4
- package/src/responses/web-search-replay.ts +138 -0
- package/src/routing/analytics.ts +7 -15
- package/src/routing/compatibility/behavior.ts +4 -1
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +2 -1
- package/src/server/chat-native.ts +28 -13
- package/src/server/claude-messages.ts +4 -2
- package/src/server/index/live-sideband.ts +3 -2
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +11 -5
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +7 -7
- package/src/server/lifecycle.ts +13 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +10 -1
- package/src/server/management/provider-completion-test.ts +289 -0
- package/src/server/management/provider-routes.ts +9 -1
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/messages-native.ts +34 -7
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +7 -2
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +34 -18
- package/src/server/responses/adapter-delivery.ts +53 -6
- package/src/server/responses/adapter-dispatch.ts +104 -29
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +6 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +8 -5
- package/src/server/responses/compaction-recovery.ts +2 -0
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-codex-account.ts +9 -3
- package/src/server/responses/core-combo.ts +34 -7
- package/src/server/responses/core-lifetime.ts +3 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +5 -1
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/native-steering-log.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +30 -33
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +1 -0
- package/src/server/responses/request-transport.ts +32 -3
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +59 -10
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/startup-action-control.ts +2 -2
- package/src/server/startup-health-cache.ts +2 -2
- package/src/server/windows-tray-control.ts +2 -2
- package/src/server/ws-bridge.ts +2 -0
- package/src/service/definition.ts +9 -1
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/opencode-zen.ts +9 -3
- package/src/types/provider.ts +9 -5
- package/src/types/request.ts +30 -4
- package/src/types/wire.ts +4 -2
- package/src/types.ts +2 -0
- package/src/usage/cost.ts +17 -1
- package/src/usage/expected-prices.ts +3 -1
- package/src/usage/jev-stats.ts +4 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +16 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/web-search/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +212 -73
- package/src/web-search/progress-stream.ts +14 -1
- package/src/web-search/run-turn-loop.ts +9 -3
- package/src/web-search/sidecar-search-replay.ts +96 -0
- package/gui/dist/assets/App-ZfYbLHkB.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/index-CfYDMj34.js +0 -94
|
@@ -46,6 +46,7 @@ export function nativeReasoningTagMatches(expected: string | undefined, presente
|
|
|
46
46
|
const MAX_TOTAL_BYTES = 256 * 1024;
|
|
47
47
|
const TTL_MS = 60 * 60 * 1000;
|
|
48
48
|
const OPAQUE_BLOB_REJECTION_TTL_MS = 5 * 60 * 1000;
|
|
49
|
+
const MAX_OPAQUE_BLOB_REJECTION_FINGERPRINTS = 256;
|
|
49
50
|
const replayIdentityKey = randomBytes(32);
|
|
50
51
|
const CREDENTIAL_HEADER_NAMES = new Set([
|
|
51
52
|
"authorization",
|
|
@@ -78,8 +79,14 @@ interface ServingIdentityEntry {
|
|
|
78
79
|
interface OpaqueBlobRejectionEntry {
|
|
79
80
|
bytes: number;
|
|
80
81
|
at: number;
|
|
82
|
+
/** Undefined retains the legacy blanket rejection; only payload digests are stored here. */
|
|
83
|
+
fingerprints?: ReadonlySet<string>;
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
export type ReasoningReplayOpaqueBlobRejection =
|
|
87
|
+
| { mode: "blanket" }
|
|
88
|
+
| { mode: "selective"; fingerprints: ReadonlySet<string> };
|
|
89
|
+
|
|
83
90
|
const entries = new Map<string, CacheEntry>();
|
|
84
91
|
const servingIdentities = new Map<string, ServingIdentityEntry>();
|
|
85
92
|
const opaqueBlobRejections = new Map<string, OpaqueBlobRejectionEntry>();
|
|
@@ -310,18 +317,57 @@ export function reasoningReplayOpaqueBlobRejectionMemoized(
|
|
|
310
317
|
return opaqueBlobRejections.has(key);
|
|
311
318
|
}
|
|
312
319
|
|
|
320
|
+
/** Undefined means no live memo; a selective result owns a defensive copy of its digests. */
|
|
321
|
+
export function getReasoningReplayOpaqueBlobRejection(
|
|
322
|
+
scope: OcxReasoningReplayScopeRef | undefined,
|
|
323
|
+
): ReasoningReplayOpaqueBlobRejection | undefined {
|
|
324
|
+
const key = opaqueBlobRejectionKeyFor(scope);
|
|
325
|
+
if (!key) return undefined;
|
|
326
|
+
sweepExpiredOpaqueBlobRejections(now());
|
|
327
|
+
const entry = opaqueBlobRejections.get(key);
|
|
328
|
+
return entry === undefined ? undefined : entry.fingerprints === undefined ? { mode: "blanket" }
|
|
329
|
+
: { mode: "selective", fingerprints: new Set(entry.fingerprints) };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Incomplete, invalid or unbounded enumerations cannot safely narrow the legacy rejection. */
|
|
333
|
+
function boundedRejectionFingerprints(fingerprints: ReadonlySet<string> | undefined): Set<string> | undefined {
|
|
334
|
+
if (fingerprints === undefined) return undefined;
|
|
335
|
+
const copy = new Set<string>();
|
|
336
|
+
try {
|
|
337
|
+
// The intrinsic validates a real Set without trusting an overridden iterator or size getter.
|
|
338
|
+
for (const value of Set.prototype.values.call(fingerprints) as IterableIterator<unknown>) {
|
|
339
|
+
if (typeof value !== "string" || value.length !== 64 || !/^[a-f0-9]{64}$/.test(value)
|
|
340
|
+
|| copy.size >= MAX_OPAQUE_BLOB_REJECTION_FINGERPRINTS) return undefined;
|
|
341
|
+
copy.add(value);
|
|
342
|
+
}
|
|
343
|
+
} catch { return undefined; }
|
|
344
|
+
return copy.size > 0 ? copy : undefined;
|
|
345
|
+
}
|
|
346
|
+
|
|
313
347
|
/** Record only after a blobless retry succeeded for this durable serving identity. */
|
|
314
348
|
export function rememberReasoningReplayOpaqueBlobRejection(
|
|
315
349
|
scope: OcxReasoningReplayScopeRef | undefined,
|
|
350
|
+
fingerprints?: ReadonlySet<string>,
|
|
316
351
|
): void {
|
|
317
352
|
const key = opaqueBlobRejectionKeyFor(scope);
|
|
318
353
|
if (!key) return;
|
|
319
|
-
const
|
|
320
|
-
if (
|
|
354
|
+
const keyBytes = Buffer.byteLength(key, "utf8");
|
|
355
|
+
if (keyBytes > MAX_TOTAL_BYTES) return;
|
|
321
356
|
const at = now();
|
|
322
357
|
sweepExpiredOpaqueBlobRejections(at);
|
|
323
|
-
|
|
324
|
-
|
|
358
|
+
const previous = opaqueBlobRejections.get(key);
|
|
359
|
+
let merged = previous && previous.fingerprints === undefined ? undefined : boundedRejectionFingerprints(fingerprints);
|
|
360
|
+
if (merged && previous?.fingerprints) {
|
|
361
|
+
for (const fingerprint of previous.fingerprints) {
|
|
362
|
+
merged.add(fingerprint);
|
|
363
|
+
if (merged.size > MAX_OPAQUE_BLOB_REJECTION_FINGERPRINTS) { merged = undefined; break; }
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// A near-limit key can still retain its existing blanket memo if the digest set will not fit.
|
|
367
|
+
if (merged && keyBytes + merged.size * 64 > MAX_TOTAL_BYTES) merged = undefined;
|
|
368
|
+
const bytes = keyBytes + (merged?.size ?? 0) * 64;
|
|
369
|
+
if (previous) deleteOpaqueBlobRejection(key);
|
|
370
|
+
opaqueBlobRejections.set(key, { bytes, at, ...(merged ? { fingerprints: merged } : {}) });
|
|
325
371
|
opaqueBlobRejectionTotalBytes += bytes;
|
|
326
372
|
while (
|
|
327
373
|
(opaqueBlobRejectionTotalBytes > MAX_TOTAL_BYTES || opaqueBlobRejections.size > MAX_ENTRIES)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse-time rebuild of replayed hosted `web_search_call` cells for routed providers (#7269).
|
|
3
|
+
*
|
|
4
|
+
* Codex replays the cell it was shown, which carries the query and at most a source list, never the
|
|
5
|
+
* result text. Dropping the cell made a routed model forget it had searched: after a client tool
|
|
6
|
+
* call ended the turn, the next request carried no trace of the search, so the model searched and
|
|
7
|
+
* ran its tools again in a loop.
|
|
8
|
+
*
|
|
9
|
+
* When search is declared, the parser rewrites each cell into the `web_search` function call and
|
|
10
|
+
* output the routed model made, at the raw-input level, so the ordinary call/output handling owns
|
|
11
|
+
* turn batching and reasoning attachment. The call takes the cell's place; its output is deferred to
|
|
12
|
+
* the end of the contiguous run of call/output items that follows, so a cell inside a parallel
|
|
13
|
+
* client batch never splits it. The output is a fallback built from the cell alone; the web-search
|
|
14
|
+
* sidecar swaps in the result it recorded for the cell, and removes the pairs again when it does not
|
|
15
|
+
* run, because the synthetic tool is then undeclared (src/web-search/sidecar-search-replay.ts).
|
|
16
|
+
*/
|
|
17
|
+
import { isObj } from "./parser-content";
|
|
18
|
+
import { WEB_SEARCH_TOOL_NAME } from "../web-search/synthetic-tool";
|
|
19
|
+
import type { OcxReplayedWebSearchCell } from "../types";
|
|
20
|
+
|
|
21
|
+
const MAX_FIELD_CHARS = 2048;
|
|
22
|
+
const MAX_LIST_ENTRIES = 20;
|
|
23
|
+
const CELL_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
24
|
+
|
|
25
|
+
/** Call items of a batch; a call after the batch's first output starts the next assistant turn. */
|
|
26
|
+
const CALL_TYPES = new Set(["function_call", "custom_tool_call", "local_shell_call", "tool_search_call"]);
|
|
27
|
+
/** Output items of a batch. Anything that is neither a call nor an output ends the batch. */
|
|
28
|
+
const OUTPUT_TYPES = new Set(["function_call_output", "custom_tool_call_output", "local_shell_call_output", "tool_search_output"]);
|
|
29
|
+
|
|
30
|
+
function capped(value: string): string {
|
|
31
|
+
return value.length <= MAX_FIELD_CHARS ? value : `${value.slice(0, MAX_FIELD_CHARS)}…`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function queriesOf(action: Record<string, unknown>): string[] {
|
|
35
|
+
const queries = Array.isArray(action.queries)
|
|
36
|
+
? action.queries.filter((query): query is string => typeof query === "string" && query.trim() !== "")
|
|
37
|
+
: [];
|
|
38
|
+
if (queries.length === 0 && typeof action.query === "string" && action.query.trim() !== "") queries.push(action.query);
|
|
39
|
+
return queries.slice(0, MAX_LIST_ENTRIES).map(capped);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sourcesOf(item: Record<string, unknown>, action: Record<string, unknown>): string[] {
|
|
43
|
+
const lines: string[] = [];
|
|
44
|
+
const seen = new Set<string>();
|
|
45
|
+
for (const list of [item.sources, action.sources]) {
|
|
46
|
+
if (!Array.isArray(list)) continue;
|
|
47
|
+
for (const source of list) {
|
|
48
|
+
if (lines.length >= MAX_LIST_ENTRIES) return lines;
|
|
49
|
+
if (!isObj(source) || typeof source.url !== "string" || seen.has(source.url)) continue;
|
|
50
|
+
seen.add(source.url);
|
|
51
|
+
lines.push(typeof source.title === "string" && source.title.trim() !== ""
|
|
52
|
+
? `- ${capped(source.title)} <${capped(source.url)}>`
|
|
53
|
+
: `- <${capped(source.url)}>`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return lines;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The call arguments and fallback result text for one replayed cell. */
|
|
60
|
+
export function replayedWebSearchContent(item: Record<string, unknown>): { arguments: Record<string, unknown>; result: string; failed: boolean } {
|
|
61
|
+
const action = isObj(item.action) ? item.action : {};
|
|
62
|
+
const failed = item.status === "failed";
|
|
63
|
+
const queries = queriesOf(action);
|
|
64
|
+
const lines: string[] = [];
|
|
65
|
+
if (action.type === "open_page" && typeof action.url === "string") {
|
|
66
|
+
lines.push(`Earlier page open: <${capped(action.url)}>.`);
|
|
67
|
+
} else if (action.type === "find_in_page" && typeof action.url === "string") {
|
|
68
|
+
lines.push(`Earlier find in page <${capped(action.url)}>${typeof action.pattern === "string" ? ` for "${capped(action.pattern)}"` : ""}.`);
|
|
69
|
+
} else {
|
|
70
|
+
lines.push(queries.length > 0 ? `Earlier web search for: ${queries.map(query => `"${query}"`).join(", ")}.` : "Earlier web search.");
|
|
71
|
+
}
|
|
72
|
+
if (failed) {
|
|
73
|
+
lines.push("This search failed and returned no results.");
|
|
74
|
+
} else {
|
|
75
|
+
lines.push("The result text of this search is not available in the replayed conversation history.");
|
|
76
|
+
const sources = sourcesOf(item, action);
|
|
77
|
+
if (sources.length > 0) lines.push("Sources it returned (titles and URLs only, untrusted):", ...sources);
|
|
78
|
+
}
|
|
79
|
+
return { arguments: queries.length > 1 ? { queries } : { query: queries[0] ?? "" }, result: lines.join("\n"), failed };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface RewrittenWebSearchInput {
|
|
83
|
+
items: unknown[];
|
|
84
|
+
/** Original input index per rewritten item, for boundaries measured on the caller's input. */
|
|
85
|
+
sourceIndexes: number[];
|
|
86
|
+
cells: OcxReplayedWebSearchCell[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Rewrite replayed cells into web_search call/output pairs; undefined when the input has none. */
|
|
90
|
+
export function rewriteReplayedWebSearchCells(input: readonly unknown[]): RewrittenWebSearchInput | undefined {
|
|
91
|
+
if (!input.some(item => isObj(item) && item.type === "web_search_call")) return undefined;
|
|
92
|
+
// A synthetic call id must not collide with any client call id, nor with another cell's.
|
|
93
|
+
const taken = new Set(input.flatMap(item => isObj(item) && typeof item.call_id === "string" ? [item.call_id] : []));
|
|
94
|
+
const allocate = (base: string): string => {
|
|
95
|
+
let id = base;
|
|
96
|
+
for (let n = 1; taken.has(id); n++) id = `${base}_${n}`;
|
|
97
|
+
taken.add(id);
|
|
98
|
+
return id;
|
|
99
|
+
};
|
|
100
|
+
const items: unknown[] = [];
|
|
101
|
+
const sourceIndexes: number[] = [];
|
|
102
|
+
const cells: OcxReplayedWebSearchCell[] = [];
|
|
103
|
+
const seenCells = new Set<string>();
|
|
104
|
+
let pending: Array<{ item: unknown; sourceIndex: number }> = [];
|
|
105
|
+
// True once the current batch reached its outputs: the next call opens a new assistant turn.
|
|
106
|
+
let outputPhase = false;
|
|
107
|
+
const flush = () => {
|
|
108
|
+
for (const entry of pending) { items.push(entry.item); sourceIndexes.push(entry.sourceIndex); }
|
|
109
|
+
pending = [];
|
|
110
|
+
outputPhase = false;
|
|
111
|
+
};
|
|
112
|
+
input.forEach((item, sourceIndex) => {
|
|
113
|
+
const type = isObj(item) ? item.type : undefined;
|
|
114
|
+
if (isObj(item) && type === "web_search_call") {
|
|
115
|
+
const cellId = typeof item.id === "string" && CELL_ID.test(item.id) ? item.id : undefined;
|
|
116
|
+
// The same cell replayed twice is one search.
|
|
117
|
+
if (cellId && seenCells.has(cellId)) return;
|
|
118
|
+
if (cellId) seenCells.add(cellId);
|
|
119
|
+
if (outputPhase) flush();
|
|
120
|
+
const content = replayedWebSearchContent(item);
|
|
121
|
+
const callId = allocate(cellId ?? "ws_replay");
|
|
122
|
+
cells.push({ ...(cellId ? { cellId } : {}), callId, failed: content.failed });
|
|
123
|
+
items.push({ type: "function_call", call_id: callId, name: WEB_SEARCH_TOOL_NAME, arguments: JSON.stringify(content.arguments) });
|
|
124
|
+
sourceIndexes.push(sourceIndex);
|
|
125
|
+
pending.push({ item: { type: "function_call_output", call_id: callId, output: content.result }, sourceIndex });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (typeof type === "string" && OUTPUT_TYPES.has(type)) {
|
|
129
|
+
outputPhase = true;
|
|
130
|
+
} else if (!(typeof type === "string" && CALL_TYPES.has(type)) || outputPhase) {
|
|
131
|
+
flush();
|
|
132
|
+
}
|
|
133
|
+
items.push(item);
|
|
134
|
+
sourceIndexes.push(sourceIndex);
|
|
135
|
+
});
|
|
136
|
+
flush();
|
|
137
|
+
return { items, sourceIndexes, cells };
|
|
138
|
+
}
|
package/src/routing/analytics.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { PersistedUsageEntry } from "../usage/log";
|
|
15
|
-
import {
|
|
15
|
+
import { computeEntryCost } from "../usage/summary";
|
|
16
16
|
import { openRequestHistoryIndex, requestHistoryDb } from "./history/indexer";
|
|
17
17
|
|
|
18
18
|
export const ANALYTICS_MAX_ROWS = 50_000;
|
|
@@ -122,6 +122,7 @@ const COOLDOWN_RECOVERY_KINDS = new Set([
|
|
|
122
122
|
"oauth-account-403",
|
|
123
123
|
"anthropic-oauth-429",
|
|
124
124
|
"oauth-account-429",
|
|
125
|
+
"oauth-account-402",
|
|
125
126
|
]);
|
|
126
127
|
|
|
127
128
|
function percentile(sorted: number[], p: number): number | undefined {
|
|
@@ -158,19 +159,10 @@ function cooldownTriggering(entry: PersistedUsageEntry | null, status: number):
|
|
|
158
159
|
});
|
|
159
160
|
}
|
|
160
161
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (!entry.usage) return null;
|
|
166
|
-
const estimate = estimateRequestCost({
|
|
167
|
-
provider: row.provider,
|
|
168
|
-
model: row.model,
|
|
169
|
-
usage: entry.usage,
|
|
170
|
-
usageStatus: entry.usageStatus,
|
|
171
|
-
serviceTier: serviceTierContext(entry),
|
|
172
|
-
});
|
|
173
|
-
return estimate ? estimate.cost.total : null;
|
|
162
|
+
/** Same attempt-aware pricing as /api/usage, so a refused retry before a measured send is priced. */
|
|
163
|
+
function successCostUsd(entry: PersistedUsageEntry): number | null {
|
|
164
|
+
const { isPriced, costTotal } = computeEntryCost(entry);
|
|
165
|
+
return isPriced ? costTotal : null;
|
|
174
166
|
}
|
|
175
167
|
|
|
176
168
|
export async function computeRoutingAnalytics(
|
|
@@ -245,7 +237,7 @@ export async function computeRoutingAnalytics(
|
|
|
245
237
|
if (kind === "success") {
|
|
246
238
|
const entry = parseEntry(row.rowJson);
|
|
247
239
|
if (entry) {
|
|
248
|
-
rowCostUsd = successCostUsd(
|
|
240
|
+
rowCostUsd = successCostUsd(entry);
|
|
249
241
|
if (rowCostUsd !== null) {
|
|
250
242
|
costTotalUsd += rowCostUsd;
|
|
251
243
|
costCount += 1;
|
|
@@ -4,6 +4,7 @@ import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
|
4
4
|
import { PROVIDER_REGISTRY } from "../../providers/registry";
|
|
5
5
|
import { fastPolicyForModel, serviceTierSupportFromPolicy } from "../../providers/service-tier";
|
|
6
6
|
import { resolveProviderAuthTransport } from "../../providers/fastwire";
|
|
7
|
+
import { defaultAnthropicCacheRetention } from "../../adapters/anthropic/cache-retention";
|
|
7
8
|
import { localFingerprint } from "../../lab/digest";
|
|
8
9
|
import type { LabBehaviorSource, LabBehaviorValues } from "../../lab/live/types";
|
|
9
10
|
|
|
@@ -237,7 +238,9 @@ export function resolveProductionBehaviorValues(
|
|
|
237
238
|
}),
|
|
238
239
|
"tools.builtinNameEscaping": behaviorRow("provider_config", effective.escapeBuiltinToolNames === true),
|
|
239
240
|
"cache.forwarding": behaviorRow("provider_config", effective.promptCacheKey === true),
|
|
240
|
-
|
|
241
|
+
// The wire TTL, not just the setting: an omitted value means 1h on first-party Anthropic.
|
|
242
|
+
"cache.retention": behaviorRow("global_config", config.cacheRetention
|
|
243
|
+
?? (adapter === "anthropic" ? defaultAnthropicCacheRetention(effective) : "short")),
|
|
241
244
|
"anthropic.eofPolicy": behaviorRow("provider_config", effective.anthropicEofTolerance === true ? "tolerant" : "strict"),
|
|
242
245
|
"openai-chat.eofPolicy": behaviorRow("provider_config", effective.openaiChatEofTolerance === true ? "tolerant" : "strict"),
|
|
243
246
|
"google.mode": behaviorRow("provider_config", effective.googleMode ?? null),
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { DECLARABLE_HOSTED_TOOL_TYPES } from "../responses/hosted-tool-policy";
|
|
|
40
40
|
import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
|
|
41
41
|
import { providerConfigSeed } from "../providers/derive";
|
|
42
42
|
import { anthropicOAuthInstanceConfigError } from "../config/schema/anthropic-account-pool";
|
|
43
|
+
import { accountAutoPausePolicySchema } from "../config/schema/account-auto-pause";
|
|
43
44
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
44
45
|
import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
|
|
45
46
|
import { modelAutoCompactTokenLimitsConfigError } from "../providers/auto-compact-budget";
|
|
@@ -740,6 +741,9 @@ export function providerManagementConfigError(
|
|
|
740
741
|
return "provider must be a plain object";
|
|
741
742
|
}
|
|
742
743
|
const raw = provider as Record<string, unknown>;
|
|
744
|
+
if (raw.accountAutoPause !== undefined && !accountAutoPausePolicySchema.safeParse(raw.accountAutoPause).success) {
|
|
745
|
+
return "accountAutoPause must be an object with optional boolean enabled and integer consecutiveFailures (1-100)";
|
|
746
|
+
}
|
|
743
747
|
const contextTiersError = contextTierRecordConfigError(raw.modelContextTiers);
|
|
744
748
|
if (contextTiersError) return contextTiersError;
|
|
745
749
|
const capabilitiesError = modelCapabilitiesConfigError(raw.modelCapabilities);
|
|
@@ -796,6 +800,8 @@ export function providerManagementConfigError(
|
|
|
796
800
|
// sends recover, not what the canonical forward seed is. Validated below
|
|
797
801
|
// (retryOnResetPolicyConfigError).
|
|
798
802
|
delete canonicalCandidate.retryOnReset;
|
|
803
|
+
// Validated account eligibility policy is independent of the canonical transport.
|
|
804
|
+
delete canonicalCandidate.accountAutoPause;
|
|
799
805
|
// Context windows are the same kind of user-owned overlay as requestPacing: the operator
|
|
800
806
|
// narrowing what their own native rows advertise. They can only ever LOWER the measured
|
|
801
807
|
// window (see nativeOpenAiContextWindow), so admitting them cannot widen what the proxy
|
|
@@ -1087,6 +1093,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
1087
1093
|
apiKeyPool: "redacted",
|
|
1088
1094
|
// Ordering preference only; it names no key material, so an editor may read and set it.
|
|
1089
1095
|
apiKeyPoolStrategy: "editor",
|
|
1096
|
+
accountAutoPause: "editor",
|
|
1090
1097
|
apiKeySelectionRevision: "runtime",
|
|
1091
1098
|
_apiKeyAttempt: "runtime",
|
|
1092
1099
|
defaultModel: "editor",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OcxConfig, StorageCleanupPolicy } from "../types";
|
|
2
2
|
import { registerCodexLowQuotaProtection, type LowQuotaRegistration } from "../codex/low-quota-protection";
|
|
3
|
+
import { resolveLowQuotaDesktopSink } from "../codex/low-quota-desktop-notifier";
|
|
3
4
|
import type { LowQuotaEvent } from "../codex/low-quota-events";
|
|
4
5
|
import { startStateStoreSweeper } from "../lib/state-store-sweeper";
|
|
5
6
|
import {
|
|
@@ -209,7 +210,7 @@ export function acquireServerBackgroundLifecycle(
|
|
|
209
210
|
fanoutRevoked: false,
|
|
210
211
|
applyPolicy,
|
|
211
212
|
resources: acquireServerResourceOwner(),
|
|
212
|
-
lowQuota: registerCodexLowQuotaProtection(config),
|
|
213
|
+
lowQuota: registerCodexLowQuotaProtection(config, { desktopSink: () => resolveLowQuotaDesktopSink() }),
|
|
213
214
|
};
|
|
214
215
|
try {
|
|
215
216
|
if (!processLoops) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { captureAccountAutoPauseAttempt, wasAccountAutoPaused } from "../oauth/account-auto-pause";
|
|
1
2
|
import { createPhysicalSendReporter } from "../lib/request-execution-budget";
|
|
2
3
|
import { createInferenceSendBudget } from "./inference/context";
|
|
3
4
|
import { observeAdvisorNativeReasoning } from "./inference/advisor-diagnostics";
|
|
@@ -64,11 +65,10 @@ import { RequestPacingQueueOverloadError } from "../providers/request-pacing";
|
|
|
64
65
|
import { stampApiKeyAccountLabel } from "../providers/label";
|
|
65
66
|
import { distributedKeyPhysicalFetch, distributedKeyResponseOwner } from "../providers/api-key-response-ownership";
|
|
66
67
|
import { providerApiKeySelectionIsCurrent, resolveCurrentProviderApiKeyTransport } from "../providers/api-key-selection";
|
|
67
|
-
import { enrichOpenCodeZenFreeTierMessage } from "../providers/opencode-zen-rate-limit";
|
|
68
68
|
import type { OcxProviderTransport } from "../providers/xai-transport";
|
|
69
69
|
import type { RouteResult } from "../router";
|
|
70
70
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
71
|
-
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, sendWithConnectionPolicy } from "./responses/fetch-helpers";
|
|
71
|
+
import { fetchWithHeaderTimeout, physicalSendObserverInit, providerFetch, safeHostLabel, sendWithConnectionPolicy } from "./responses/fetch-helpers";
|
|
72
72
|
import { isLocalUpstream } from "../lib/local-upstream";
|
|
73
73
|
import { linkAbortSignal } from "./responses";
|
|
74
74
|
import {
|
|
@@ -449,7 +449,7 @@ export async function runNativeChatAttempt(
|
|
|
449
449
|
let responseOwnsDistributedKey = true;
|
|
450
450
|
const send = async (
|
|
451
451
|
request: AdapterRequest,
|
|
452
|
-
recovery?: "rate-limit-429" | "key-
|
|
452
|
+
recovery?: "rate-limit-429" | "key-401" | "key-429" | UpstreamSendRecovery,
|
|
453
453
|
singleShot = false,
|
|
454
454
|
): Promise<Response> => {
|
|
455
455
|
try {
|
|
@@ -516,6 +516,7 @@ export async function runNativeChatAttempt(
|
|
|
516
516
|
const spendReport = createPhysicalSendReporter(physicalBudget, () => ({
|
|
517
517
|
poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
|
|
518
518
|
}), execution.comboDispatchPermit);
|
|
519
|
+
const observeAutoPause = captureAccountAutoPauseAttempt(route.providerName, activeProvider, { config, oauth: undefined }, headers);
|
|
519
520
|
let dispatched: Response;
|
|
520
521
|
try {
|
|
521
522
|
// Capture this request's spend policy at its first physical boundary, including
|
|
@@ -538,6 +539,7 @@ export async function runNativeChatAttempt(
|
|
|
538
539
|
request.url,
|
|
539
540
|
applyUpstreamRecoveryInit({
|
|
540
541
|
...init, method: request.method, headers, body: request.body,
|
|
542
|
+
...physicalSendObserverInit(observeAutoPause, request.url),
|
|
541
543
|
}, transportRecovery),
|
|
542
544
|
// Reselection can replace the provider transport and the wire shape, so the
|
|
543
545
|
// egress route is bound to the provider this send actually uses. Omitting it
|
|
@@ -552,7 +554,7 @@ export async function runNativeChatAttempt(
|
|
|
552
554
|
responseOwnsDistributedKey = !activeProvider._apiKeyAttempt?.distributed
|
|
553
555
|
|| distributedKeyResponseOwner(dispatched, config, route.providerName) !== null;
|
|
554
556
|
if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
|
|
555
|
-
return dispatched;
|
|
557
|
+
return observeAutoPause ? observeAutoPause(dispatched) : dispatched;
|
|
556
558
|
},
|
|
557
559
|
}),
|
|
558
560
|
);
|
|
@@ -585,6 +587,23 @@ export async function runNativeChatAttempt(
|
|
|
585
587
|
response = await send(activeRequest);
|
|
586
588
|
const retryPolicy = rateLimitRetryPolicyFor(activeProvider);
|
|
587
589
|
let retries = 0;
|
|
590
|
+
let autoPauseMoves = 0;
|
|
591
|
+
for (;;) {
|
|
592
|
+
const priorResponse = response;
|
|
593
|
+
for (; wasAccountAutoPaused(response) && config.providers[route.providerName]?.apiKey && autoPauseMoves < 3 && transientSendAvailable(); autoPauseMoves++) {
|
|
594
|
+
const fresh = resolveCurrentProviderApiKeyTransport(config, route.providerName, { ...activeProvider, _apiKeyAttempt: undefined });
|
|
595
|
+
const next = fresh && (selectProactiveApiKeyTransport(config, route.providerName, fresh) ?? fresh);
|
|
596
|
+
if (!next) break;
|
|
597
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* closed */ }
|
|
598
|
+
activeProvider = next;
|
|
599
|
+
stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
|
|
600
|
+
activeAdapter = createOpenAIChatAdapter(activeProvider);
|
|
601
|
+
releaseRetainedRequest();
|
|
602
|
+
activeRequest = buildActiveRequest();
|
|
603
|
+
retainRequest(activeRequest);
|
|
604
|
+
response = await send(activeRequest, "key-401");
|
|
605
|
+
}
|
|
606
|
+
|
|
588
607
|
while (
|
|
589
608
|
response.status === 429
|
|
590
609
|
// A 429 this proxy synthesized for a refused reset replay is not a provider rate
|
|
@@ -632,6 +651,10 @@ export async function runNativeChatAttempt(
|
|
|
632
651
|
retainRequest(activeRequest);
|
|
633
652
|
response = await send(activeRequest, failureStatus === 401 ? "key-401" : "key-429");
|
|
634
653
|
}
|
|
654
|
+
if (response !== priorResponse && wasAccountAutoPaused(response)) continue;
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
|
|
635
658
|
} catch (error) {
|
|
636
659
|
releaseRetainedRequest();
|
|
637
660
|
cleanupAbort();
|
|
@@ -694,20 +717,12 @@ export async function runNativeChatAttempt(
|
|
|
694
717
|
&& (isCyberPolicyCode(upstreamCode) || isCyberPolicyMessage(upstreamMessage))
|
|
695
718
|
? upstreamMessage
|
|
696
719
|
: detail ? `Provider error ${response.status}: ${detail}` : `Provider error ${response.status}`;
|
|
697
|
-
// Zen's keyless free tier refuses the request outright rather than rate-limiting it, and
|
|
698
|
-
// the raw `MissingSessionID` tells a user nothing about why or what to do (#4121).
|
|
699
|
-
const clientMessage = enrichOpenCodeZenFreeTierMessage(message, {
|
|
700
|
-
providerName: route.providerName,
|
|
701
|
-
baseUrl: route.provider.baseUrl,
|
|
702
|
-
adapter: route.provider.adapter,
|
|
703
|
-
upstreamErrorType: upstreamType,
|
|
704
|
-
});
|
|
705
720
|
const classified = classifyError(
|
|
706
721
|
response.status,
|
|
707
722
|
upstreamType ?? (response.status === 401 ? "authentication_error"
|
|
708
723
|
: response.status === 429 ? "rate_limit_error"
|
|
709
724
|
: response.status >= 500 ? "server_error" : "invalid_request_error"),
|
|
710
|
-
|
|
725
|
+
message,
|
|
711
726
|
);
|
|
712
727
|
// The verdict is read once, from the response that carries it and from the code a
|
|
713
728
|
// re-wrapped body kept -- never from the status, which a real rate limit shares.
|
|
@@ -33,7 +33,7 @@ import { resolveAlias, claudeCodeNativeAlias, legacyAliasForNative } from "../cl
|
|
|
33
33
|
import { recordDesktopRequest } from "../claude/desktop-health";
|
|
34
34
|
import { stripOneMillionMarker } from "../claude/context-windows";
|
|
35
35
|
import { captureClaudeInbound } from "../claude/inbound-debug";
|
|
36
|
-
import { declaredAnthropicServerTools } from "../claude/inbound-content-options";
|
|
36
|
+
import { declaredAnthropicServerTools, replayedAnthropicServerBlocks } from "../claude/inbound-content-options";
|
|
37
37
|
import { claudeCodeForIngress } from "../claude/intercept/model-bindings";
|
|
38
38
|
import { classifyInterceptClient } from "../claude/intercept/client-class";
|
|
39
39
|
import { analyzeClaudeCompatibility, isClaudeCompatibilityMode } from "../claude/compatibility";
|
|
@@ -1110,7 +1110,8 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1110
1110
|
const finalizeNativeLog = createFinalRequestLog(logIds, logCtx).finish;
|
|
1111
1111
|
const upstream = await handleResponses(internalReq, {
|
|
1112
1112
|
...buildClaudeReplayConfig(config),
|
|
1113
|
-
// Operator config wins; otherwise the caller's
|
|
1113
|
+
// Operator config wins; otherwise the caller's explicit TTLs (all 1h, or a pinned 5m) carry
|
|
1114
|
+
// over, and TTL-less breakpoints take the destination default.
|
|
1114
1115
|
cacheRetention: config.cacheRetention ?? callerCacheRetention(anthropicBody),
|
|
1115
1116
|
}, logCtx, {
|
|
1116
1117
|
// Routing keeps Claude-only sidecar overrides; admission policy must follow the live owner.
|
|
@@ -1133,6 +1134,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1133
1134
|
nativeReasoningMint,
|
|
1134
1135
|
// Out of body like the replay above: the core hands these to an Anthropic destination only.
|
|
1135
1136
|
anthropicServerTools: declaredAnthropicServerTools(anthropicBody),
|
|
1137
|
+
anthropicServerReplay: replayedAnthropicServerBlocks(anthropicBody),
|
|
1136
1138
|
claudeGoAffinity: { sessionLane: claudeGoSessionLane },
|
|
1137
1139
|
claudeNativeSessionId,
|
|
1138
1140
|
stripClaudeMainAuthForNoncanonicalForward: true,
|
|
@@ -40,8 +40,9 @@ export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
|
40
40
|
*
|
|
41
41
|
* What must never happen is neither: an immortal socket plus a destination that silently accepts
|
|
42
42
|
* the orphaned delta. `tests/responses/ws-endpoint.test.ts` holds exactly that pair together.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* Responses admission separately reclaims its least recently used idle connection under
|
|
44
|
+
* capacity pressure. That is not a time limit, so expired-reference validation remains required.
|
|
45
|
+
* Keeping this shared timeout at 0 also preserves quiet live/voice sessions.
|
|
45
46
|
*/
|
|
46
47
|
export const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
|
|
47
48
|
/** Ceiling a finite websocket idle timeout must stay under, in seconds. See above. */
|
|
@@ -17,6 +17,7 @@ import { createLinkSupervisor, type LinkSupervisor } from "../../link/supervisor
|
|
|
17
17
|
import { linkManagementRouteAllowed } from "../../link/management-routes";
|
|
18
18
|
import { saveConfigPreservingClaudeCode } from "../../config/live-reconcile";
|
|
19
19
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
20
|
+
import { startJevQuotaActivation } from "../../combos/jev-quota-activation";
|
|
20
21
|
export { LINK_INGRESS_HOSTNAME } from "./link-listener";
|
|
21
22
|
import type { ServerIngress } from "./serve-options";
|
|
22
23
|
|
|
@@ -51,6 +52,7 @@ export function createOptionalListenerSet<T>(linkDeps: LinkListenerDeps = {}): O
|
|
|
51
52
|
const claudeIntercept: ClaudeInterceptLifecycle<T> = createClaudeInterceptLifecycle<T>();
|
|
52
53
|
const linkListener: LinkListenerLifecycle<T> = createLinkListenerLifecycle<T>(linkDeps);
|
|
53
54
|
let activeConfig: OcxConfig | undefined;
|
|
55
|
+
let stopJevQuotaActivation: (() => void) | undefined;
|
|
54
56
|
const supervisor = createLinkSupervisor({
|
|
55
57
|
apiKeys: () => activeConfig?.apiKeys ?? [],
|
|
56
58
|
revokeApiKey: id => {
|
|
@@ -114,6 +116,8 @@ export function createOptionalListenerSet<T>(linkDeps: LinkListenerDeps = {}): O
|
|
|
114
116
|
maxRequestBodySize: ctx.maxRequestBodySize,
|
|
115
117
|
dispatch: ctx.dispatch,
|
|
116
118
|
});
|
|
119
|
+
// Last, after every listener started: JEV advisory quota follows the live config while serving.
|
|
120
|
+
stopJevQuotaActivation = startJevQuotaActivation(ctx.config);
|
|
117
121
|
},
|
|
118
122
|
ensureStarted: () => linkListener.ensureStarted(),
|
|
119
123
|
close: () => linkListener.close(),
|
|
@@ -132,6 +136,8 @@ export function createOptionalListenerSet<T>(linkDeps: LinkListenerDeps = {}): O
|
|
|
132
136
|
try { await claudeIntercept.stop(); } catch (error) { failure ??= error; }
|
|
133
137
|
unregisterSupervisorAdmission?.();
|
|
134
138
|
unregisterSupervisorAdmission = undefined;
|
|
139
|
+
stopJevQuotaActivation?.();
|
|
140
|
+
stopJevQuotaActivation = undefined;
|
|
135
141
|
if (failure) throw failure;
|
|
136
142
|
},
|
|
137
143
|
};
|
|
@@ -1618,11 +1618,17 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1618
1618
|
// Promote a caller `x-session-id` before admission, as Responses and Messages do (#6520):
|
|
1619
1619
|
// the ChatGPT bridge forwards only canonical session headers, so the raw marker is lost.
|
|
1620
1620
|
const sessionReq = withCallerSessionIdentity(req, admission);
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1621
|
+
// Only expose an id when the handler or a logged refusal owns its history row.
|
|
1622
|
+
let ownsRequestLog = false;
|
|
1623
|
+
const response = await runAdmittedHttpTurn(sessionReq, policy, async turnAdmissionLease => {
|
|
1624
|
+
ownsRequestLog = true;
|
|
1625
|
+
return withCors(
|
|
1626
|
+
await handleChatCompletions(sessionReq, config, logCtx, { requestId, start, turnAdmissionLease, admission }),
|
|
1627
|
+
req,
|
|
1628
|
+
policy,
|
|
1629
|
+
);
|
|
1630
|
+
}, { requestId, start, logCtx, onLogged: () => { ownsRequestLog = true; } });
|
|
1631
|
+
return ownsRequestLog ? withRequestLogId(response, requestId) : response;
|
|
1626
1632
|
}
|
|
1627
1633
|
|
|
1628
1634
|
if (url.pathname === "/v1/audio/transcriptions" && req.method === "POST") {
|
|
@@ -89,6 +89,7 @@ import {
|
|
|
89
89
|
import type { ServeOptionsContext } from "./serve-options";
|
|
90
90
|
import type { RequestMetricsRecorder } from "../request-metrics";
|
|
91
91
|
import { resolveInboundBodyLimitBytes } from "../request-decompress";
|
|
92
|
+
import { responseToolOwnership } from "../responses/response-tool-ownership";
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
95
|
* The WebSocket half of the Bun.serve options, split out of serve-options.ts to keep that file
|
|
@@ -184,6 +185,8 @@ export function createWebsocketHandler(
|
|
|
184
185
|
}
|
|
185
186
|
return;
|
|
186
187
|
}
|
|
188
|
+
if (ws.data.responsesActivity?.closing) return;
|
|
189
|
+
ws.data.responsesActivity?.touch();
|
|
187
190
|
const rawBytes = typeof raw === "string" ? Buffer.byteLength(raw) : raw.byteLength;
|
|
188
191
|
if (rawBytes > MAX_WS_FRAME_BYTES) {
|
|
189
192
|
sendJsonFrame(ws, buildWsErrorFrame(413, {
|
|
@@ -223,6 +226,7 @@ export function createWebsocketHandler(
|
|
|
223
226
|
if (frame.type === "response.inject") {
|
|
224
227
|
if (!ws.data.nativeControl?.inject) throw new NativeSteeringError("injection_not_supported", "Native injection is disabled or unavailable on this route.");
|
|
225
228
|
ws.data.nativeControl.inject(frame);
|
|
229
|
+
ws.data.responsesActivity?.acceptResults(frame.input);
|
|
226
230
|
return;
|
|
227
231
|
}
|
|
228
232
|
if (frame.type === "response.steer") {
|
|
@@ -230,7 +234,10 @@ export function createWebsocketHandler(
|
|
|
230
234
|
ws.data.nativeControl.steer(frame);
|
|
231
235
|
return;
|
|
232
236
|
}
|
|
233
|
-
if (ws.data.nativeControl?.continue(frame))
|
|
237
|
+
if (ws.data.nativeControl?.continue(frame)) {
|
|
238
|
+
ws.data.responsesActivity?.acceptResults(frame.input);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
234
241
|
} catch (error) {
|
|
235
242
|
sendJsonFrame(ws, buildWsErrorFrame(400, {
|
|
236
243
|
type: "invalid_request_error",
|
|
@@ -294,6 +301,7 @@ export function createWebsocketHandler(
|
|
|
294
301
|
}
|
|
295
302
|
|
|
296
303
|
// Only a genuinely admitted turn may receive steering or continuations.
|
|
304
|
+
const finishActivity = ws.data.responsesActivity?.beginTurn(frame.input);
|
|
297
305
|
ws.data.nativeControl = nativeControl;
|
|
298
306
|
const payload: Record<string, unknown> = { ...frame };
|
|
299
307
|
delete payload.type;
|
|
@@ -346,11 +354,16 @@ export function createWebsocketHandler(
|
|
|
346
354
|
terminalRecorder = recorder;
|
|
347
355
|
},
|
|
348
356
|
});
|
|
357
|
+
const observeLog = nativeControl?.relayActive
|
|
358
|
+
? createNativeSteeringLogObserver(logCtx, () => recordFirstOutput(logCtx, start))
|
|
359
|
+
: (payload: string) => inspectResponseLogSsePayload(logCtx, payload);
|
|
360
|
+
const providerExecuted = responseToolOwnership(response);
|
|
349
361
|
await sendResponseToWebSocket(ws, response, isCurrent, {
|
|
350
362
|
untilEof: nativeControl?.relayActive === true,
|
|
351
|
-
onSsePayload:
|
|
352
|
-
|
|
353
|
-
|
|
363
|
+
onSsePayload: payload => {
|
|
364
|
+
ws.data.responsesActivity?.observePayload(payload, providerExecuted);
|
|
365
|
+
observeLog(payload);
|
|
366
|
+
},
|
|
354
367
|
onTerminal: status => {
|
|
355
368
|
terminalRecorder?.(status, logCtx.terminalHttpStatus);
|
|
356
369
|
finalizeLog(httpStatusForRequestLogTerminal(status, logCtx), {
|
|
@@ -392,6 +405,7 @@ export function createWebsocketHandler(
|
|
|
392
405
|
/* socket already gone or send dropped */
|
|
393
406
|
}
|
|
394
407
|
} finally {
|
|
408
|
+
finishActivity?.();
|
|
395
409
|
turnAdmissionLease.release();
|
|
396
410
|
if (ws.data.nativeControl === nativeControl) ws.data.nativeControl = undefined;
|
|
397
411
|
if (!logged && turnAbort.signal.aborted) finalizeLog(499);
|