@bitkyc08/opencodex 2.55.0 → 2.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Read-ahead allowance, not a lifetime limit on SSE inspection or delivery. */
|
|
2
|
+
export const MAX_INSPECTION_READ_AHEAD_BYTES = 32 * 1024 * 1024;
|
|
3
|
+
|
|
4
|
+
export interface InspectionTeeOptions {
|
|
5
|
+
/** Release pacing so the inspection owner's existing bounded drain can run. */
|
|
6
|
+
clientGoneSignal?: AbortSignal;
|
|
7
|
+
/** Internal test seam; callers must not take this value from upstream data. */
|
|
8
|
+
maxReadAheadBytes?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Keep native tee cancellation semantics while pacing the inspection branch.
|
|
13
|
+
*
|
|
14
|
+
* Inspection may lead raw client consumption by the allowance plus one source
|
|
15
|
+
* chunk (and native tee prefetch). It never stops merely because the whole turn
|
|
16
|
+
* crossed that size: terminal, usage and continuation observers retain ownership.
|
|
17
|
+
* Count raw client bytes BEFORE rewrites, which may shrink, drop or expand them.
|
|
18
|
+
* Push sources still need their own producer-side bound; this is not an RSS cap.
|
|
19
|
+
*/
|
|
20
|
+
export function teeWithBoundedInspection(
|
|
21
|
+
source: ReadableStream<Uint8Array>,
|
|
22
|
+
options: InspectionTeeOptions = {},
|
|
23
|
+
): [ReadableStream<Uint8Array>, ReadableStream<Uint8Array>] {
|
|
24
|
+
const limit = options.maxReadAheadBytes ?? MAX_INSPECTION_READ_AHEAD_BYTES;
|
|
25
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
26
|
+
throw new RangeError("Inspection read-ahead limit must be a positive safe integer");
|
|
27
|
+
}
|
|
28
|
+
const [client, inspection] = source.tee();
|
|
29
|
+
let leadBytes = 0;
|
|
30
|
+
let pacingReleased = false;
|
|
31
|
+
let credit: Promise<void> | undefined;
|
|
32
|
+
let wake: (() => void) | undefined;
|
|
33
|
+
|
|
34
|
+
const wakeReader = () => {
|
|
35
|
+
const resolve = wake;
|
|
36
|
+
wake = undefined;
|
|
37
|
+
credit = undefined;
|
|
38
|
+
resolve?.();
|
|
39
|
+
};
|
|
40
|
+
const releasePacing = () => {
|
|
41
|
+
pacingReleased = true;
|
|
42
|
+
wakeReader();
|
|
43
|
+
};
|
|
44
|
+
const signal = options.clientGoneSignal;
|
|
45
|
+
if (signal?.aborted) releasePacing();
|
|
46
|
+
else signal?.addEventListener("abort", releasePacing, { once: true });
|
|
47
|
+
|
|
48
|
+
const wrap = (
|
|
49
|
+
body: ReadableStream<Uint8Array>,
|
|
50
|
+
isClient: boolean,
|
|
51
|
+
): ReadableStream<Uint8Array> => {
|
|
52
|
+
const reader = body.getReader();
|
|
53
|
+
let ended = false;
|
|
54
|
+
// An upstream error must wake an inspector waiting for credit, not remain
|
|
55
|
+
// hidden until the client happens to issue another read. One observer per
|
|
56
|
+
// reader, not a permanent Promise.race reaction attached on every chunk.
|
|
57
|
+
void reader.closed.catch(releasePacing);
|
|
58
|
+
const releaseLock = () => {
|
|
59
|
+
try { reader.releaseLock(); } catch { /* a pending read owns the lock */ }
|
|
60
|
+
};
|
|
61
|
+
const finish = () => {
|
|
62
|
+
ended = true;
|
|
63
|
+
releasePacing();
|
|
64
|
+
if (!isClient) signal?.removeEventListener("abort", releasePacing);
|
|
65
|
+
};
|
|
66
|
+
return new ReadableStream<Uint8Array>({
|
|
67
|
+
async pull(controller) {
|
|
68
|
+
if (ended) return;
|
|
69
|
+
if (!isClient && !pacingReleased && leadBytes >= limit) {
|
|
70
|
+
credit ??= new Promise<void>(resolve => { wake = resolve; });
|
|
71
|
+
await credit;
|
|
72
|
+
if (ended) return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const result = await reader.read();
|
|
76
|
+
if (ended) return;
|
|
77
|
+
if (result.done) {
|
|
78
|
+
finish();
|
|
79
|
+
releaseLock();
|
|
80
|
+
controller.close();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (isClient) {
|
|
84
|
+
leadBytes -= result.value.byteLength;
|
|
85
|
+
if (leadBytes < limit) wakeReader();
|
|
86
|
+
} else {
|
|
87
|
+
leadBytes += result.value.byteLength;
|
|
88
|
+
}
|
|
89
|
+
controller.enqueue(result.value);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (ended) return;
|
|
92
|
+
finish();
|
|
93
|
+
releaseLock();
|
|
94
|
+
controller.error(error);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
cancel(reason) {
|
|
98
|
+
finish();
|
|
99
|
+
// Awaiting one tee branch's cancellation waits for the sibling. The
|
|
100
|
+
// owner must be free to finish cleanup and cancel/read that sibling.
|
|
101
|
+
void reader.cancel(reason).catch(() => undefined);
|
|
102
|
+
releaseLock();
|
|
103
|
+
},
|
|
104
|
+
}, { highWaterMark: 0 });
|
|
105
|
+
};
|
|
106
|
+
return [wrap(client, true), wrap(inspection, false)];
|
|
107
|
+
}
|
package/src/server/live.ts
CHANGED
|
@@ -72,7 +72,17 @@ export const LIVE_SIDEBAND_API_ROOT = "https://api.openai.com/v1";
|
|
|
72
72
|
* Client protocol headers relayed verbatim to the upstream on call-create and sideband upgrade.
|
|
73
73
|
* `openai-alpha: quicksilver=v2` carries the Frameless protocol negotiation — without it the
|
|
74
74
|
* ChatGPT backend validates the type-less Frameless session as v1 quicksilver and 400s
|
|
75
|
-
* (openai/codex `realtime_request_headers`, core/src/realtime_conversation.rs).
|
|
75
|
+
* (openai/codex `realtime_request_headers`, core/src/realtime_conversation.rs).
|
|
76
|
+
*
|
|
77
|
+
* `x-codex-turn-metadata` is on the same list upstream builds for the sideband upgrade and was
|
|
78
|
+
* missing here, so every realtime turn reached the model with metadata the client had attached
|
|
79
|
+
* and this proxy silently dropped. The Responses passthrough already forwards it
|
|
80
|
+
* (`src/adapters/openai-responses/passthrough.ts`); the sideband goes to the same realtime
|
|
81
|
+
* upstream the caller was addressing, so there is nothing to scope it away from. That is not
|
|
82
|
+
* true of the images sidecar, which strips it deliberately and keeps doing so.
|
|
83
|
+
*
|
|
84
|
+
* Every name here is relayed only when the caller sent it. Nothing on this list is invented,
|
|
85
|
+
* which is what keeps a caller that omits one byte-identical upstream. Auth headers
|
|
76
86
|
* (`authorization`, `chatgpt-account-id`) stay proxy-owned and are never taken from this list.
|
|
77
87
|
*/
|
|
78
88
|
export const LIVE_CLIENT_PROTOCOL_HEADERS = [
|
|
@@ -82,6 +92,7 @@ export const LIVE_CLIENT_PROTOCOL_HEADERS = [
|
|
|
82
92
|
"thread-id",
|
|
83
93
|
"originator",
|
|
84
94
|
"x-oai-attestation",
|
|
95
|
+
"x-codex-turn-metadata",
|
|
85
96
|
] as const;
|
|
86
97
|
|
|
87
98
|
/**
|
|
@@ -128,6 +139,40 @@ export function logLiveSidebandFrame(dir: "c2u" | "u2c", data: unknown): void {
|
|
|
128
139
|
}
|
|
129
140
|
}
|
|
130
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Sideband lifecycle stages, recorded in the same JSONL as the frame records.
|
|
144
|
+
*
|
|
145
|
+
* Frame forensics alone cannot separate the three realtime-voice failures reported in #4721.
|
|
146
|
+
* A join that never reached this proxy, a join whose upstream handshake was refused, and a
|
|
147
|
+
* relay that opened and then carried nothing all leave the same empty file, which is why the
|
|
148
|
+
* original report could only say "no frame log". One record per stage makes them distinct:
|
|
149
|
+
* no record at all means the client never dialed the proxy, `upstream-failed` carries the
|
|
150
|
+
* status the client was handed, and `relay-attached` with no following frame record means the
|
|
151
|
+
* transport is live and the silence is upstream of it.
|
|
152
|
+
*/
|
|
153
|
+
export type LiveSidebandStage = "upstream-open" | "upstream-failed" | "relay-attached" | "relay-closed";
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Append one lifecycle record. Same privacy rule as the frame records and for the same reason:
|
|
157
|
+
* no URL, no call id, no header, no frame content — only the stage and, on failure, the status
|
|
158
|
+
* and error code this proxy synthesized itself.
|
|
159
|
+
*/
|
|
160
|
+
export function logLiveSidebandStage(
|
|
161
|
+
stage: LiveSidebandStage,
|
|
162
|
+
detail?: { status?: number; code?: string },
|
|
163
|
+
): void {
|
|
164
|
+
const logPath = process.env[LIVE_FRAME_LOG_ENV];
|
|
165
|
+
if (!logPath) return;
|
|
166
|
+
try {
|
|
167
|
+
const record: Record<string, unknown> = { ts: new Date().toISOString(), stage };
|
|
168
|
+
if (detail?.status !== undefined) record.status = detail.status;
|
|
169
|
+
if (detail?.code !== undefined) record.code = detail.code;
|
|
170
|
+
appendFileSync(logPath, JSON.stringify(record) + "\n");
|
|
171
|
+
} catch {
|
|
172
|
+
// Diagnostics must never break the relay.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
131
176
|
function clientProtocolHeaders(reqHeaders: Headers): Record<string, string> {
|
|
132
177
|
const out: Record<string, string> = {};
|
|
133
178
|
for (const name of LIVE_CLIENT_PROTOCOL_HEADERS) {
|
|
@@ -80,17 +80,20 @@ function sparseComboConfig<T extends {
|
|
|
80
80
|
waitForCooldownMs?: number;
|
|
81
81
|
imageInput?: "auto" | "disabled";
|
|
82
82
|
reasoningEffortMode?: "strict" | "adaptive";
|
|
83
|
-
|
|
83
|
+
defaultEffortMode?: "fallback" | "force";
|
|
84
|
+
}>(combo: T): Omit<T, "cooldownMs" | "waitForCooldownMs" | "imageInput" | "reasoningEffortMode" | "defaultEffortMode"> & {
|
|
84
85
|
cooldownMs?: number;
|
|
85
86
|
waitForCooldownMs?: number;
|
|
86
87
|
imageInput?: "disabled";
|
|
87
88
|
reasoningEffortMode?: "adaptive";
|
|
89
|
+
defaultEffortMode?: "force";
|
|
88
90
|
} {
|
|
89
91
|
const {
|
|
90
92
|
cooldownMs,
|
|
91
93
|
waitForCooldownMs,
|
|
92
94
|
imageInput,
|
|
93
95
|
reasoningEffortMode,
|
|
96
|
+
defaultEffortMode,
|
|
94
97
|
...rest
|
|
95
98
|
} = combo;
|
|
96
99
|
return {
|
|
@@ -101,6 +104,7 @@ function sparseComboConfig<T extends {
|
|
|
101
104
|
: {}),
|
|
102
105
|
...(imageInput === "disabled" ? { imageInput: "disabled" as const } : {}),
|
|
103
106
|
...(reasoningEffortMode === "adaptive" ? { reasoningEffortMode: "adaptive" as const } : {}),
|
|
107
|
+
...(defaultEffortMode === "force" ? { defaultEffortMode: "force" as const } : {}),
|
|
104
108
|
};
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -170,6 +174,11 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
170
174
|
...(!Object.hasOwn(requestedCombo, "waitForCooldownMs") && previous?.waitForCooldownMs !== undefined
|
|
171
175
|
? { waitForCooldownMs: previous.waitForCooldownMs }
|
|
172
176
|
: {}),
|
|
177
|
+
// The dashboard does not expose this advanced CLI/API policy. Preserve it when
|
|
178
|
+
// a GUI round-trip omits the field instead of silently downgrading to fallback.
|
|
179
|
+
...(!Object.hasOwn(requestedCombo, "defaultEffortMode") && previous?.defaultEffortMode !== undefined
|
|
180
|
+
? { defaultEffortMode: previous.defaultEffortMode }
|
|
181
|
+
: {}),
|
|
173
182
|
};
|
|
174
183
|
const error = comboConfigError(id, effectiveCombo, config.providers, {
|
|
175
184
|
requireEnabledTarget: true,
|
|
@@ -86,34 +86,34 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
86
86
|
// server/management-api
|
|
87
87
|
{ method: "POST", path: "/api/stop", module: "server/management-api", mutates: true },
|
|
88
88
|
// codex/auth-api
|
|
89
|
-
{ method: "DELETE", path: "/api/codex-auth/accounts", module: "codex/auth-api", mutates: true },
|
|
90
|
-
{ method: "GET", path: "/api/codex-auth/accounts", module: "codex/auth-api", mutates: false },
|
|
91
|
-
{ method: "GET", path: "/api/codex-auth/active", module: "codex/auth-api", mutates: false },
|
|
92
|
-
{ method: "GET", path: "/api/codex-auth/login-status", module: "codex/auth-api", mutates: false },
|
|
93
|
-
{ method: "GET", path: "/api/codex-auth/quota", module: "codex/auth-api", mutates: false },
|
|
94
|
-
{ method: "GET", path: "/api/codex-auth/quota/history", module: "codex/auth-api", mutates: false },
|
|
95
|
-
{ method: "GET", path: "/api/codex-auth/reset-credits", module: "codex/auth-api", mutates: false },
|
|
96
|
-
{ method: "PATCH", path: "/api/codex-auth/pool-strategy", module: "codex/auth-api", mutates: true },
|
|
97
|
-
{ method: "POST", path: "/api/codex-auth/accounts", module: "codex/auth-api", mutates: true },
|
|
98
|
-
{ method: "POST", path: "/api/codex-auth/accounts/clear-cooldown", module: "codex/auth-api", mutates: true },
|
|
99
|
-
{ method: "POST", path: "/api/codex-auth/accounts/refresh", module: "codex/auth-api", mutates: true },
|
|
89
|
+
{ method: "DELETE", path: "/api/codex-auth/accounts", module: "codex/auth-api/routes", mutates: true },
|
|
90
|
+
{ method: "GET", path: "/api/codex-auth/accounts", module: "codex/auth-api/routes", mutates: false },
|
|
91
|
+
{ method: "GET", path: "/api/codex-auth/active", module: "codex/auth-api/routes", mutates: false },
|
|
92
|
+
{ method: "GET", path: "/api/codex-auth/login-status", module: "codex/auth-api/routes", mutates: false },
|
|
93
|
+
{ method: "GET", path: "/api/codex-auth/quota", module: "codex/auth-api/routes", mutates: false },
|
|
94
|
+
{ method: "GET", path: "/api/codex-auth/quota/history", module: "codex/auth-api/routes", mutates: false },
|
|
95
|
+
{ method: "GET", path: "/api/codex-auth/reset-credits", module: "codex/auth-api/routes", mutates: false },
|
|
96
|
+
{ method: "PATCH", path: "/api/codex-auth/pool-strategy", module: "codex/auth-api/routes", mutates: true },
|
|
97
|
+
{ method: "POST", path: "/api/codex-auth/accounts", module: "codex/auth-api/routes", mutates: true },
|
|
98
|
+
{ method: "POST", path: "/api/codex-auth/accounts/clear-cooldown", module: "codex/auth-api/routes", mutates: true },
|
|
99
|
+
{ method: "POST", path: "/api/codex-auth/accounts/refresh", module: "codex/auth-api/routes", mutates: true },
|
|
100
100
|
// codex/main-device-reauth-api (#3898): the native-main device reauth namespace;
|
|
101
101
|
// /api/codex-auth/login stays pool-only and keeps rejecting __main__.
|
|
102
102
|
{ method: "POST", path: "/api/codex-auth/main/reauth-device", module: "codex/main-device-reauth-api", mutates: true },
|
|
103
103
|
{ method: "GET", path: "/api/codex-auth/main/reauth-device", module: "codex/main-device-reauth-api", mutates: false },
|
|
104
104
|
{ method: "DELETE", path: "/api/codex-auth/main/reauth-device", module: "codex/main-device-reauth-api", mutates: true },
|
|
105
|
-
{ method: "POST", path: "/api/codex-auth/login", module: "codex/auth-api", mutates: true },
|
|
106
|
-
{ method: "POST", path: "/api/codex-auth/login/cancel", module: "codex/auth-api", mutates: true },
|
|
107
|
-
{ method: "POST", path: "/api/codex-auth/login/code", module: "codex/auth-api", mutates: true },
|
|
108
|
-
{ method: "POST", path: "/api/codex-auth/reset-credits/consume", module: "codex/auth-api", mutates: true },
|
|
109
|
-
{ method: "PUT", path: "/api/codex-auth/accounts/alias", module: "codex/auth-api", mutates: true },
|
|
110
|
-
{ method: "PUT", path: "/api/codex-auth/accounts/pause", module: "codex/auth-api", mutates: true },
|
|
111
|
-
{ method: "PUT", path: "/api/codex-auth/accounts/pause-exhausted", module: "codex/auth-api", mutates: true },
|
|
112
|
-
{ method: "PUT", path: "/api/codex-auth/accounts/priority", module: "codex/auth-api", mutates: true },
|
|
113
|
-
{ method: "PUT", path: "/api/codex-auth/active", module: "codex/auth-api", mutates: true },
|
|
114
|
-
{ method: "PUT", path: "/api/codex-auth/auto-switch", module: "codex/auth-api", mutates: true },
|
|
115
|
-
{ method: "PUT", path: "/api/codex-auth/failover", module: "codex/auth-api", mutates: true },
|
|
116
|
-
{ method: "PUT", path: "/api/codex-auth/pool-strategy", module: "codex/auth-api", mutates: true, exempt: { reason: "compatibility-alias", why: "Superseded by PUT /api/pool/settings, which the CLI now drives. Kept working for existing clients and pinned by exact-body goldens in tests/server/account-pool-management-api.test.ts; no CLI verb targets it any more." } },
|
|
105
|
+
{ method: "POST", path: "/api/codex-auth/login", module: "codex/auth-api/routes", mutates: true },
|
|
106
|
+
{ method: "POST", path: "/api/codex-auth/login/cancel", module: "codex/auth-api/routes", mutates: true },
|
|
107
|
+
{ method: "POST", path: "/api/codex-auth/login/code", module: "codex/auth-api/routes", mutates: true },
|
|
108
|
+
{ method: "POST", path: "/api/codex-auth/reset-credits/consume", module: "codex/auth-api/routes", mutates: true },
|
|
109
|
+
{ method: "PUT", path: "/api/codex-auth/accounts/alias", module: "codex/auth-api/routes", mutates: true },
|
|
110
|
+
{ method: "PUT", path: "/api/codex-auth/accounts/pause", module: "codex/auth-api/routes", mutates: true },
|
|
111
|
+
{ method: "PUT", path: "/api/codex-auth/accounts/pause-exhausted", module: "codex/auth-api/routes", mutates: true },
|
|
112
|
+
{ method: "PUT", path: "/api/codex-auth/accounts/priority", module: "codex/auth-api/routes", mutates: true },
|
|
113
|
+
{ method: "PUT", path: "/api/codex-auth/active", module: "codex/auth-api/routes", mutates: true },
|
|
114
|
+
{ method: "PUT", path: "/api/codex-auth/auto-switch", module: "codex/auth-api/routes", mutates: true },
|
|
115
|
+
{ method: "PUT", path: "/api/codex-auth/failover", module: "codex/auth-api/routes", mutates: true },
|
|
116
|
+
{ method: "PUT", path: "/api/codex-auth/pool-strategy", module: "codex/auth-api/routes", mutates: true, exempt: { reason: "compatibility-alias", why: "Superseded by PUT /api/pool/settings, which the CLI now drives. Kept working for existing clients and pinned by exact-body goldens in tests/server/account-pool-management-api.test.ts; no CLI verb targets it any more." } },
|
|
117
117
|
// codex/native-profile-api
|
|
118
118
|
{ method: "GET", path: "/api/native-main-profiles", module: "codex/native-profile-api", mutates: false },
|
|
119
119
|
{ method: "GET", path: "/api/native-main-profiles/doctor", module: "codex/native-profile-api", mutates: false },
|
|
@@ -308,6 +308,9 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
308
308
|
{ method: "PUT", path: "/api/provider-context-caps", module: "server/management/provider-routes", mutates: true },
|
|
309
309
|
// server/management/quota-reset-routes
|
|
310
310
|
{ method: "GET", path: "/api/quota-resets", module: "server/management/quota-reset-routes", mutates: false, mechanism: "negated-guard" },
|
|
311
|
+
// server/management/workflow-budget-routes
|
|
312
|
+
{ method: "GET", path: "/api/workflow-budget", module: "server/management/workflow-budget-routes", mutates: false, exempt: { reason: "deferred-verb", why: "Reading a root's live budget is owed a CLI verb -- an operator staring at a 429 is usually already in a terminal -- but the ledger is process memory with no local transport to read it through, so the verb has to be an HTTP call the CLI does not yet make.", owner: "260915_workflow_budget_window wfc", ownerDoc: "devlog/_plan/260915_workflow_budget_window/030_wfc_diff_plan.md" } },
|
|
313
|
+
{ method: "POST", path: "/api/workflow-budget/clear", module: "server/management/workflow-budget-routes", mutates: true, exempt: { reason: "deferred-verb", why: "Clearing one root is owed the same verb as the read above and for the same reason. It is deliberately not shipped as a verb in this work-phase: the read comes first, because an operator who cannot see which ceiling fired has no basis for deciding to forgive it.", owner: "260915_workflow_budget_window wfc", ownerDoc: "devlog/_plan/260915_workflow_budget_window/030_wfc_diff_plan.md" } },
|
|
311
314
|
// server/management/request-history-routes
|
|
312
315
|
{ method: "GET", path: "/api/request-history", module: "server/management/request-history-routes", mutates: false },
|
|
313
316
|
// server/management/routing-analytics-routes
|
|
@@ -37,7 +37,7 @@ import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap
|
|
|
37
37
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
38
38
|
import { readUsageEntries } from "../../usage/log";
|
|
39
39
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
40
|
-
import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
|
|
40
|
+
import { cacheObservationFromUsage, parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
|
|
41
41
|
import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
|
|
42
42
|
import { getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
|
|
43
43
|
import { getDebugLogEntries } from "../../lib/debug-log-buffer";
|
|
@@ -97,7 +97,7 @@ export type CostResult =
|
|
|
97
97
|
| { kind: "value"; estimate: NonNullable<ReturnType<typeof estimateRequestCost>>; estimateReasons: CostEstimateReason[] }
|
|
98
98
|
| { kind: "unavailable"; reason: MetricUnavailableReason };
|
|
99
99
|
|
|
100
|
-
export type MetricSource = Pick<RequestLogEntry, "provider" | "model" | "durationMs" | "firstOutputMs" | "usageStatus" | "usage" | "requestedServiceTier" | "configuredServiceTier" | "responseServiceTier" | "tierOutcome" | "routeDecision"> & {
|
|
100
|
+
export type MetricSource = Pick<RequestLogEntry, "provider" | "model" | "durationMs" | "firstOutputMs" | "usageStatus" | "usage" | "requestedServiceTier" | "configuredServiceTier" | "responseServiceTier" | "tierOutcome" | "routeDecision" | "cacheProvenance"> & {
|
|
101
101
|
attempts?: readonly PersistedUsageAttempt[];
|
|
102
102
|
};
|
|
103
103
|
|
|
@@ -186,9 +186,12 @@ export function costResult(entry: MetricSource): CostResult {
|
|
|
186
186
|
if (!estimate) return { kind: "unavailable", reason: unavailableCostReason(entry) };
|
|
187
187
|
const estimateReasons = [
|
|
188
188
|
entry.usageStatus === "estimated" || entry.usage?.estimated ? "usage_estimated" as const : undefined,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
// A cost estimate is qualified by cache detail it can TRUST. A detail object that exists only
|
|
190
|
+
// because a strict client requires the field carries no cache reading, so it qualifies the
|
|
191
|
+
// estimate exactly as a missing one does — reading it as a measured zero prices the request
|
|
192
|
+
// as an uncached send that nothing observed.
|
|
193
|
+
entry.usage && cacheObservationFromUsage(entry.usage, entry.cacheProvenance).provenance !== "observed"
|
|
194
|
+
? "cache_detail_missing" as const : undefined,
|
|
192
195
|
estimate.price?.source === "expected" || estimate.attempts?.some(a => a.price.source === "expected")
|
|
193
196
|
? "expected_price_overlay" as const : undefined,
|
|
194
197
|
estimate.price?.source === "user" || estimate.attempts?.some(a => a.price.source === "user")
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator view of the in-memory workflow-budget ledger, plus a targeted clear.
|
|
3
|
+
*
|
|
4
|
+
* Loaded on demand from src/server/management-api.ts, which is the FOURTH entry in the protected
|
|
5
|
+
* set of tests/core-lab-boundary.test.ts — added precisely because eagerly importing handlers
|
|
6
|
+
* there put ~70 modules on every dashboard request. A static import here would make this
|
|
7
|
+
* subsystem the next instance of that bug.
|
|
8
|
+
*
|
|
9
|
+
* Authentication is inherited: every /api route passes through requireManagementAuth before the
|
|
10
|
+
* chain runs, so these handlers add no auth code of their own. The GET spends no user identity.
|
|
11
|
+
* The POST clears one root's windowed count ceilings; it does not spend identity, and the
|
|
12
|
+
* underlying ledger leaves in-flight concurrency and the token spend record untouched.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { jsonResponse } from "../auth-cors";
|
|
16
|
+
import type { OcxConfig } from "../../types";
|
|
17
|
+
import type { ManagementContext } from "./context";
|
|
18
|
+
import { readManagementJsonBodyOr } from "./body";
|
|
19
|
+
import {
|
|
20
|
+
clearWorkflowBudgetForRoot,
|
|
21
|
+
listTrackedWorkflowRoots,
|
|
22
|
+
listWorkflowBudgetEvents,
|
|
23
|
+
workflowBudgetSnapshot,
|
|
24
|
+
WORKFLOW_EVENT_CAPACITY,
|
|
25
|
+
} from "../../lib/workflow-budget";
|
|
26
|
+
|
|
27
|
+
const DEFAULT_LIMIT = 20;
|
|
28
|
+
const MAX_LIMIT = WORKFLOW_EVENT_CAPACITY;
|
|
29
|
+
const MAX_ROOT_ID_LENGTH = 200;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A root id is an opaque caller-thread token, not a path. Without a length cap a client
|
|
33
|
+
* could POST a multi-megabyte string that we would then store as a map key and echo back
|
|
34
|
+
* in events; 200 is well above any thread id we have seen and small enough to put in a URL.
|
|
35
|
+
*/
|
|
36
|
+
function parseRootId(raw: unknown): string | null {
|
|
37
|
+
if (typeof raw !== "string") return null;
|
|
38
|
+
const trimmed = raw.trim();
|
|
39
|
+
if (!trimmed || trimmed.length > MAX_ROOT_ID_LENGTH) return null;
|
|
40
|
+
return trimmed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function invalidRootResponse(req: Request, config: OcxConfig): Response {
|
|
44
|
+
return jsonResponse(
|
|
45
|
+
{ error: { code: "invalid_root", message: "root must be a non-empty string of at most 200 characters" } },
|
|
46
|
+
400,
|
|
47
|
+
req,
|
|
48
|
+
config,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseLimitParam(rawLimit: string | null): { ok: true; limit: number } | { ok: false } {
|
|
53
|
+
if (rawLimit !== null && !/^\d+$/.test(rawLimit)) return { ok: false };
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
limit: rawLimit === null ? DEFAULT_LIMIT : Math.min(MAX_LIMIT, Number.parseInt(rawLimit, 10)),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function handleWorkflowBudgetRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
61
|
+
const { url, req, config } = ctx;
|
|
62
|
+
|
|
63
|
+
if (url.pathname === "/api/workflow-budget") {
|
|
64
|
+
if (req.method !== "GET") return null;
|
|
65
|
+
|
|
66
|
+
const parsedLimit = parseLimitParam(url.searchParams.get("limit"));
|
|
67
|
+
if (!parsedLimit.ok) {
|
|
68
|
+
return jsonResponse(
|
|
69
|
+
{ error: { code: "invalid_limit", message: "limit must be a non-negative integer" } },
|
|
70
|
+
400,
|
|
71
|
+
req,
|
|
72
|
+
config,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const { limit } = parsedLimit;
|
|
76
|
+
|
|
77
|
+
const rawRoot = url.searchParams.get("root");
|
|
78
|
+
if (rawRoot !== null) {
|
|
79
|
+
const rootId = parseRootId(rawRoot);
|
|
80
|
+
if (rootId === null) return invalidRootResponse(req, config);
|
|
81
|
+
const snapshot = workflowBudgetSnapshot(rootId);
|
|
82
|
+
// An unknown id is a 200 with `root: null`, not a 404: the operator asked what this
|
|
83
|
+
// process currently holds for that token, and "nothing" is a legitimate answer. POST
|
|
84
|
+
// /clear is the opposite — claiming to forgive a ceiling that was never tracked would
|
|
85
|
+
// report a success that did not happen.
|
|
86
|
+
return jsonResponse(
|
|
87
|
+
{
|
|
88
|
+
root: snapshot ? { rootId, ...snapshot } : null,
|
|
89
|
+
events: listWorkflowBudgetEvents(WORKFLOW_EVENT_CAPACITY)
|
|
90
|
+
.filter((event) => event.rootId === rootId)
|
|
91
|
+
.slice(0, limit),
|
|
92
|
+
},
|
|
93
|
+
200,
|
|
94
|
+
req,
|
|
95
|
+
config,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return jsonResponse(
|
|
100
|
+
{
|
|
101
|
+
roots: listTrackedWorkflowRoots(limit),
|
|
102
|
+
events: listWorkflowBudgetEvents(limit),
|
|
103
|
+
},
|
|
104
|
+
200,
|
|
105
|
+
req,
|
|
106
|
+
config,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (url.pathname === "/api/workflow-budget/clear") {
|
|
111
|
+
if (req.method !== "POST") return null;
|
|
112
|
+
|
|
113
|
+
const body = await readManagementJsonBodyOr(req, {});
|
|
114
|
+
const rawRoot = body && typeof body === "object" && !Array.isArray(body)
|
|
115
|
+
? (body as { root?: unknown }).root
|
|
116
|
+
: undefined;
|
|
117
|
+
const rootId = parseRootId(rawRoot);
|
|
118
|
+
if (rootId === null) return invalidRootResponse(req, config);
|
|
119
|
+
|
|
120
|
+
const before = clearWorkflowBudgetForRoot(rootId);
|
|
121
|
+
if (!before) {
|
|
122
|
+
return jsonResponse(
|
|
123
|
+
{ error: { code: "unknown_root", message: "root is not currently tracked" } },
|
|
124
|
+
404,
|
|
125
|
+
req,
|
|
126
|
+
config,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return jsonResponse({ cleared: true, root: rootId, before }, 200, req, config);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
@@ -144,6 +144,17 @@ async function handleQuotaResetRoutesOnDemand(ctx: ManagementContext): Promise<R
|
|
|
144
144
|
return handleQuotaResetRoutes(ctx);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Lazy like the Lab and routing-profile handlers, and for the same recorded reason: this file is
|
|
149
|
+
* mounted for every dashboard request, so a static import would put the workflow-budget ledger
|
|
150
|
+
* on all of them.
|
|
151
|
+
*/
|
|
152
|
+
async function handleWorkflowBudgetRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
|
|
153
|
+
if (!pathInManagementNamespace(ctx.url.pathname, "/api/workflow-budget", true)) return null;
|
|
154
|
+
const { handleWorkflowBudgetRoutes } = await import("./management/workflow-budget-routes");
|
|
155
|
+
return handleWorkflowBudgetRoutes(ctx);
|
|
156
|
+
}
|
|
157
|
+
|
|
147
158
|
async function handleGrokCouponRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
|
|
148
159
|
if (!pathInManagementNamespace(ctx.url.pathname, "/api/grok/reset-coupons", true)) return null;
|
|
149
160
|
const { handleGrokCouponRoutes } = await import("./management/grok-coupon-routes");
|
|
@@ -263,6 +274,7 @@ export async function handleManagementAPI(
|
|
|
263
274
|
?? (await handleLogsUsageRoutes(ctx))
|
|
264
275
|
?? (await handleRequestHistoryRoutes(ctx))
|
|
265
276
|
?? (await handleQuotaResetRoutesOnDemand(ctx))
|
|
277
|
+
?? (await handleWorkflowBudgetRoutesOnDemand(ctx))
|
|
266
278
|
?? (await handleGrokCouponRoutesOnDemand(ctx))
|
|
267
279
|
?? (await handleRoutingAnalyticsRoutes(ctx))
|
|
268
280
|
?? (await handleRoutingProfileRoutesOnDemand(ctx))
|
|
@@ -466,6 +466,8 @@ export function relaySseEagerBounded(
|
|
|
466
466
|
else hooks.onSynthetic(syntheticKind, syntheticReason);
|
|
467
467
|
}
|
|
468
468
|
if (cancelled && !hooks.sawTerminal()) {
|
|
469
|
+
// Finalize transport telemetry before the cancellation hook persists its usage row.
|
|
470
|
+
upstream.abort();
|
|
469
471
|
hooks.onClientCancel();
|
|
470
472
|
}
|
|
471
473
|
if (cancelled || upstream.signal.aborted || syntheticKind === "failed" || deliveryFallbackSent) {
|
package/src/server/relay.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
MAX_CLIENT_SSE_FRAME_BYTES,
|
|
27
27
|
} from "./sse-frame-buffer";
|
|
28
28
|
import { replaceSseDataPayload } from "./sse-payload-rewrite";
|
|
29
|
+
import { createBoundedResponseLogBody } from "./response-log-body";
|
|
29
30
|
|
|
30
31
|
const nativePassthroughSseResponses = new WeakSet<Response>();
|
|
31
32
|
const eagerRelaySseResponses = new WeakSet<Response>();
|
|
@@ -718,25 +719,16 @@ export function responseWithDeferredRequestLog(
|
|
|
718
719
|
}
|
|
719
720
|
if (!response.body || !contentType.includes("text/event-stream")) {
|
|
720
721
|
if (response.body && (contentType.includes("application/json") || response.status >= 400)) {
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const body = new ReadableStream<Uint8Array>({
|
|
732
|
-
async start(controller) {
|
|
733
|
-
try {
|
|
734
|
-
controller.enqueue(new TextEncoder().encode(await finalizeJsonLog()));
|
|
735
|
-
controller.close();
|
|
736
|
-
} catch (err) {
|
|
737
|
-
addFinalRequestLog(requestId, start, logCtx, 502, { closeReason: "non_stream" }, addLog);
|
|
738
|
-
try { controller.error(err); } catch { /* already torn down */ }
|
|
739
|
-
}
|
|
722
|
+
const body = createBoundedResponseLogBody(response.body, {
|
|
723
|
+
json: contentType.includes("application/json"),
|
|
724
|
+
inspect: text => inspectResponseLogJson(logCtx, text),
|
|
725
|
+
finalize: reason => {
|
|
726
|
+
// Preserve wire status; request history follows the adjacent SSE
|
|
727
|
+
// convention for a client cancellation or upstream read failure.
|
|
728
|
+
const status = reason === "cancel" ? 499 : reason === "read_error" ? 502 : response.status;
|
|
729
|
+
addFinalRequestLog(requestId, start, logCtx, status, {
|
|
730
|
+
closeReason: reason === "cancel" ? "client_cancel" : "non_stream",
|
|
731
|
+
}, addLog);
|
|
740
732
|
},
|
|
741
733
|
});
|
|
742
734
|
return new Response(body, {
|
|
@@ -1338,6 +1330,9 @@ function startBoundedInspectionPump(options: InspectionPumpOptions): void {
|
|
|
1338
1330
|
try {
|
|
1339
1331
|
for (;;) {
|
|
1340
1332
|
const { done, value } = await reader.read();
|
|
1333
|
+
// Hard cancellation settles a pending read as EOF. Do not flush a
|
|
1334
|
+
// partial terminal after the owner already finalized cancellation.
|
|
1335
|
+
if (cancelled) break;
|
|
1341
1336
|
if (clientGoneSignal?.aborted) markClientGone();
|
|
1342
1337
|
if (drainStopped) {
|
|
1343
1338
|
// stopDrain() cancelled the reader; the settled read is the wake-up.
|
|
@@ -64,13 +64,16 @@ export function sessionIdHeaderFromRequest(headers: Headers): string | null {
|
|
|
64
64
|
/**
|
|
65
65
|
* Fixed-size logical turn lane (#820).
|
|
66
66
|
*
|
|
67
|
-
* A lane must be as SPECIFIC as the identity available
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
67
|
+
* A lane must be as SPECIFIC as the identity available. `codexPoolAffinityKey` used to be the
|
|
68
|
+
* opposite: it preferred the parent thread, so a whole subagent fan-out shared one entry, and a
|
|
69
|
+
* lane keyed that way would have put every parallel subagent of one parent into a single lane
|
|
70
|
+
* and rejected all but the first with 503 — the fan-out is the normal case, not an abuse.
|
|
71
|
+
* Since #4546 affinity keys every thread as ITSELF and reads the parent only as a first-placement
|
|
72
|
+
* hint, so the two now agree on the unit. They still derive it differently: a lane is a digest an
|
|
73
|
+
* operator can match against what the client sent, while an affinity key is an opaque HMAC
|
|
74
|
+
* precisely so no caller-supplied identifier ends up in Pool state.
|
|
72
75
|
*
|
|
73
|
-
*
|
|
76
|
+
* The parent stays a QUALIFIER here, never the lane on its own when a child thread exists: the
|
|
74
77
|
* pair separates siblings while still keeping one conversation's overlapping turns together.
|
|
75
78
|
*/
|
|
76
79
|
export function sessionLaneIdFromRequest(headers: Headers): string | undefined {
|
|
@@ -256,4 +259,3 @@ export function getOrAllocateRequestSessionLane(req: Request): string {
|
|
|
256
259
|
export function linkRequestSessionLane(sourceReq: Request, targetReq: Request): void {
|
|
257
260
|
requestAllocatedSessionLanes.set(targetReq, getOrAllocateRequestSessionLane(sourceReq));
|
|
258
261
|
}
|
|
259
|
-
|