@bitkyc08/opencodex 2.84.0 → 2.86.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 +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-BjlttVLy.js +58 -0
- package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
- package/gui/dist/assets/index-BMssP4k3.js +94 -0
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +87 -8
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +33 -4
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/turn.ts +22 -9
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +2 -2
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/qoder/adapter.ts +40 -1
- package/src/advisor/consult.ts +220 -0
- package/src/advisor/context.ts +226 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/runtime.ts +305 -0
- package/src/advisor/settings.ts +135 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +5 -0
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +22 -8
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/inbound.ts +121 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +13 -6
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +1 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +2 -1
- package/src/cli/index.ts +13 -4
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/registry.ts +15 -3
- package/src/cli/restart-scope.ts +3 -1
- package/src/clients/config-export/contracts.ts +1 -0
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +17 -4
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -1
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +5 -0
- package/src/codex/catalog/provider-models.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +22 -4
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/config-write-lock.ts +40 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +4 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +55 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +173 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- package/src/config/diagnostics.ts +2 -1
- package/src/config/live-reconcile.ts +13 -0
- package/src/config/load-degrade.ts +6 -1
- package/src/config/paths.ts +10 -3
- package/src/config/schema/config-schema.ts +17 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +451 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +107 -0
- package/src/lib/errors.ts +7 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/index.ts +4 -2
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/registry/entries-core.ts +7 -6
- package/src/providers/registry/entries-extended.ts +19 -0
- package/src/responses/parser.ts +30 -19
- package/src/responses/state.ts +32 -29
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +248 -216
- package/src/routing/history/ingest.ts +238 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/server/admission-model-scope.ts +4 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index/startup-warnings.ts +9 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +13 -10
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +191 -0
- package/src/server/management/agent-settings-routes.ts +22 -4
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/route-registry.ts +11 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +6 -0
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +5 -1
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +35 -18
- package/src/server/responses/adapter-delivery.ts +24 -2
- package/src/server/responses/adapter-dispatch.ts +29 -2
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-slot.ts +336 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- package/src/server/responses/core-combo-failure.ts +10 -1
- package/src/server/responses/core-combo.ts +21 -0
- package/src/server/responses/core-lifetime.ts +2 -0
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-options.ts +46 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +16 -10
- package/src/server/responses/passthrough-dispatch.ts +14 -3
- package/src/server/responses/request-prepare.ts +68 -5
- package/src/server/responses/request-transport.ts +16 -0
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +78 -3
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- package/src/service/desktop-startup.ts +14 -0
- package/src/service/health-sentinel-main.ts +23 -4
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +63 -6
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +15 -0
- package/src/types/tools.ts +2 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/expected-prices.ts +12 -0
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core-owned registration slot for the optional advisor subsystem (src/advisor).
|
|
3
|
+
*
|
|
4
|
+
* This file is the ONLY thing the core Responses path knows about the advisor. It holds the
|
|
5
|
+
* structural plan interface and the event-stream guard — pure protocol machinery over
|
|
6
|
+
* src/types — and imports nothing from src/advisor at runtime. The optional subsystem registers
|
|
7
|
+
* a factory through advisor-plan-slot at host activation; an advisor-disabled install therefore executes no advisor
|
|
8
|
+
* code and imports no advisor module (same seam discipline as src/lab).
|
|
9
|
+
*
|
|
10
|
+
* Guard semantics (mirrors guardTerminalEventStream):
|
|
11
|
+
* - synthetic `advisor` tool-call events are HELD — the Codex client never sees the tool, the
|
|
12
|
+
* call, or its arguments;
|
|
13
|
+
* - on a clean `done`, each held call is consulted through the plan, the advice is appended as a
|
|
14
|
+
* paired assistant-toolCall + toolResult message pair, and the worker is re-dispatched via the
|
|
15
|
+
* SAME continuation machinery the terminal guard uses;
|
|
16
|
+
* - real (non-advisor) tool calls end interception for the leg: the turn belongs to the client;
|
|
17
|
+
* - usage from intercepted legs is merged into the final terminal event so worker accounting
|
|
18
|
+
* stays complete; the advisor's own usage is a separate loopback request and never merges here;
|
|
19
|
+
* - consultations and worker continuations have separate hard per-request bounds. Exhaustion
|
|
20
|
+
* removes the tool; one final limit-result continuation is allowed, then a typed error ends it.
|
|
21
|
+
*/
|
|
22
|
+
import type {
|
|
23
|
+
AdapterEvent,
|
|
24
|
+
OcxAssistantContentPart,
|
|
25
|
+
OcxAssistantMessage,
|
|
26
|
+
OcxMessage,
|
|
27
|
+
OcxParsedRequest,
|
|
28
|
+
OcxToolResultMessage,
|
|
29
|
+
OcxUsage,
|
|
30
|
+
} from "../../types";
|
|
31
|
+
import { mergeUsage } from "./terminal-guard";
|
|
32
|
+
|
|
33
|
+
export const ADVISOR_TOOL_NAME = "advisor";
|
|
34
|
+
|
|
35
|
+
/** Hard bound on advisor consultations per worker request (recursion guard). */
|
|
36
|
+
export const MAX_ADVISOR_CONSULTATIONS_PER_REQUEST = 3;
|
|
37
|
+
/** At most one final worker continuation after consultation exhaustion. */
|
|
38
|
+
export const MAX_ADVISOR_CONTINUATIONS_PER_REQUEST = MAX_ADVISOR_CONSULTATIONS_PER_REQUEST + 1;
|
|
39
|
+
|
|
40
|
+
/** Per-leg retention caps for rebuilding the assistant message (see terminal-guard's bounded retention). */
|
|
41
|
+
const MAX_LEG_TEXT_CHARS = 16 * 1_024;
|
|
42
|
+
const MAX_LEG_THINKING_CHARS = 16 * 1_024;
|
|
43
|
+
|
|
44
|
+
/** Outcome of one consultation, as the plan reports it to the guard. */
|
|
45
|
+
export interface AdvisorConsultOutcome {
|
|
46
|
+
ok: boolean;
|
|
47
|
+
/** Formatted, wrapper-marked advice text ready to hand to the worker. */
|
|
48
|
+
content: string;
|
|
49
|
+
isError: boolean;
|
|
50
|
+
/** The consultation was cancelled by the caller (client abort) — not a provider failure. */
|
|
51
|
+
cancelled?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Operator config blocked the consultation before any outbound call. Not a provider
|
|
54
|
+
* failure: preflight must release a claim instead of recording a cooldown.
|
|
55
|
+
*/
|
|
56
|
+
blocked?: "consent" | "settings";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The structural plan the optional advisor subsystem registers per request. */
|
|
60
|
+
export interface AdvisorPlan {
|
|
61
|
+
/**
|
|
62
|
+
* Run one consultation for the current conversation state. Implementations own dedup,
|
|
63
|
+
* logging, and usage accounting; the guard only consumes the outcome.
|
|
64
|
+
*/
|
|
65
|
+
consult(
|
|
66
|
+
parsed: OcxParsedRequest,
|
|
67
|
+
reason: "manual",
|
|
68
|
+
question: string | undefined,
|
|
69
|
+
): Promise<AdvisorConsultOutcome>;
|
|
70
|
+
/**
|
|
71
|
+
* Runtime-owned text for a failed or limited consultation. The guard never composes failure
|
|
72
|
+
* prose itself: the implementation neutralizes untrusted text so no guard path can emit
|
|
73
|
+
* something the provenance detector would read as genuine advice.
|
|
74
|
+
*/
|
|
75
|
+
formatUnavailable(kind: "manual" | "limit", error: string): string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface HeldAdvisorCall {
|
|
79
|
+
id: string;
|
|
80
|
+
name: string;
|
|
81
|
+
argsBuf: string;
|
|
82
|
+
closed: boolean;
|
|
83
|
+
providerMetadata?: import("../../types").OcxProviderOpaqueToolCallMetadata;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseAdvisorArgs(argsBuf: string): { question?: string } {
|
|
87
|
+
if (argsBuf.trim() === "") return {};
|
|
88
|
+
try {
|
|
89
|
+
const parsed: unknown = JSON.parse(argsBuf);
|
|
90
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
91
|
+
const question = (parsed as { question?: unknown }).question;
|
|
92
|
+
if (typeof question === "string" && question.trim() !== "") return { question: question.slice(0, 2_000) };
|
|
93
|
+
}
|
|
94
|
+
} catch { /* malformed args → no focus question */ }
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function assistantMessageFromLeg(
|
|
99
|
+
events: readonly AdapterEvent[],
|
|
100
|
+
held: readonly HeldAdvisorCall[],
|
|
101
|
+
timestamp: number,
|
|
102
|
+
): OcxAssistantMessage | undefined {
|
|
103
|
+
let text = "";
|
|
104
|
+
let thinking = "";
|
|
105
|
+
let signature: string | undefined;
|
|
106
|
+
const redacted: string[] = [];
|
|
107
|
+
for (const event of events) {
|
|
108
|
+
if (event.type === "text_delta") text += event.text;
|
|
109
|
+
else if (event.type === "thinking_delta") thinking += event.thinking;
|
|
110
|
+
else if (event.type === "thinking_signature") signature = event.signature;
|
|
111
|
+
else if (event.type === "redacted_thinking") redacted.push(event.data);
|
|
112
|
+
}
|
|
113
|
+
if (text.length > MAX_LEG_TEXT_CHARS) text = text.slice(0, MAX_LEG_TEXT_CHARS);
|
|
114
|
+
if (thinking.length > MAX_LEG_THINKING_CHARS) thinking = thinking.slice(0, MAX_LEG_THINKING_CHARS);
|
|
115
|
+
const content: OcxAssistantContentPart[] = [];
|
|
116
|
+
if (thinking || signature || redacted.length > 0) {
|
|
117
|
+
content.push({ type: "thinking", thinking, ...(signature ? { signature } : {}), ...(redacted.length > 0 ? { redacted } : {}) });
|
|
118
|
+
}
|
|
119
|
+
if (text) content.push({ type: "text", text });
|
|
120
|
+
for (const call of held) {
|
|
121
|
+
if (!call.closed) continue;
|
|
122
|
+
content.push({
|
|
123
|
+
type: "toolCall",
|
|
124
|
+
id: call.id,
|
|
125
|
+
name: call.name,
|
|
126
|
+
arguments: parseAdvisorArgs(call.argsBuf),
|
|
127
|
+
...(call.providerMetadata ? { providerMetadata: call.providerMetadata } : {}),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (content.length === 0) return undefined;
|
|
131
|
+
return { role: "assistant", content, timestamp };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function advisorToolResult(
|
|
135
|
+
call: HeldAdvisorCall,
|
|
136
|
+
outcome: AdvisorConsultOutcome,
|
|
137
|
+
timestamp: number,
|
|
138
|
+
): OcxToolResultMessage {
|
|
139
|
+
return {
|
|
140
|
+
role: "toolResult",
|
|
141
|
+
toolCallId: call.id,
|
|
142
|
+
toolName: ADVISOR_TOOL_NAME,
|
|
143
|
+
content: outcome.content,
|
|
144
|
+
isError: outcome.isError,
|
|
145
|
+
timestamp,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface AdvisorGuardOptions {
|
|
150
|
+
parsed: OcxParsedRequest;
|
|
151
|
+
plan: AdvisorPlan;
|
|
152
|
+
firstEvents: AsyncIterable<AdapterEvent>;
|
|
153
|
+
/** One bounded worker continuation re-dispatch (same machinery as the terminal guard). */
|
|
154
|
+
continuation: (parsed: OcxParsedRequest) => AsyncIterable<AdapterEvent> | Promise<AsyncIterable<AdapterEvent>>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Wrap a worker event stream with advisor interception. Registered on the parsed request as
|
|
159
|
+
* `_advisorGuard` by the sidecar planner; adapter delivery applies it when present.
|
|
160
|
+
*/
|
|
161
|
+
export function createAdvisorStreamGuard(options: AdvisorGuardOptions): AsyncGenerator<AdapterEvent> {
|
|
162
|
+
const guard = createAdvisorGuard(options.plan);
|
|
163
|
+
return guard(options);
|
|
164
|
+
}
|
|
165
|
+
export function createAdvisorGuard(plan: AdvisorPlan): NonNullable<OcxParsedRequest["_advisorGuard"]> {
|
|
166
|
+
// Shared across invocations of this request's guard, including an empty-completion retry.
|
|
167
|
+
let consultations = 0;
|
|
168
|
+
let continuations = 0;
|
|
169
|
+
let finalContinuationUsed = false;
|
|
170
|
+
return async function* guardAdvisorStream(options: Omit<AdvisorGuardOptions, "plan">): AsyncGenerator<AdapterEvent> {
|
|
171
|
+
const maxConsultations = MAX_ADVISOR_CONSULTATIONS_PER_REQUEST;
|
|
172
|
+
let parsed = options.parsed;
|
|
173
|
+
let accumulatedUsage: OcxUsage | undefined;
|
|
174
|
+
let source: AsyncIterable<AdapterEvent> = options.firstEvents;
|
|
175
|
+
|
|
176
|
+
while (true) {
|
|
177
|
+
const held: HeldAdvisorCall[] = [];
|
|
178
|
+
const legEvents: AdapterEvent[] = [];
|
|
179
|
+
let legTextChars = 0;
|
|
180
|
+
let legThinkingChars = 0;
|
|
181
|
+
let pending: HeldAdvisorCall | null = null;
|
|
182
|
+
// While a tool call is open, its delta/end events belong to that call. Advisor calls are
|
|
183
|
+
// held from the output; real calls pass through untouched.
|
|
184
|
+
let holdingCurrent = false;
|
|
185
|
+
let hasRealToolCall = false;
|
|
186
|
+
let terminalConsumed = false;
|
|
187
|
+
let terminalEvent: Extract<AdapterEvent, { type: "done" }> | undefined;
|
|
188
|
+
|
|
189
|
+
for await (const event of source) {
|
|
190
|
+
if (event.type === "tool_call_start") {
|
|
191
|
+
if (pending) { held.push(pending); pending = null; }
|
|
192
|
+
pending = { id: event.id, name: event.name, argsBuf: "", closed: false, ...(event.providerMetadata ? { providerMetadata: event.providerMetadata } : {}) };
|
|
193
|
+
holdingCurrent = event.name === ADVISOR_TOOL_NAME;
|
|
194
|
+
if (!holdingCurrent) hasRealToolCall = true;
|
|
195
|
+
else continue;
|
|
196
|
+
} else if (event.type === "tool_call_delta") {
|
|
197
|
+
if (pending) pending.argsBuf += event.arguments;
|
|
198
|
+
if (holdingCurrent) continue;
|
|
199
|
+
} else if (event.type === "tool_call_end") {
|
|
200
|
+
if (pending) {
|
|
201
|
+
pending.closed = true;
|
|
202
|
+
if (pending.name === ADVISOR_TOOL_NAME) held.push(pending);
|
|
203
|
+
pending = null;
|
|
204
|
+
}
|
|
205
|
+
if (holdingCurrent) {
|
|
206
|
+
holdingCurrent = false;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
} else if (event.type === "done") {
|
|
210
|
+
if (pending) { held.push(pending); pending = null; }
|
|
211
|
+
terminalEvent = event;
|
|
212
|
+
terminalConsumed = true;
|
|
213
|
+
break;
|
|
214
|
+
} else if (event.type === "incomplete" || event.type === "error") {
|
|
215
|
+
// A broken leg cannot safely anchor a continuation: surface the terminal as-is.
|
|
216
|
+
if (pending) { pending = null; }
|
|
217
|
+
const usage = mergeUsage(accumulatedUsage, event.usage);
|
|
218
|
+
yield usage ? { ...event, usage } : event;
|
|
219
|
+
return;
|
|
220
|
+
} else {
|
|
221
|
+
// Retain (bounded) the leg's visible content so the rebuilt assistant message is complete.
|
|
222
|
+
if (event.type === "text_delta") {
|
|
223
|
+
legTextChars += event.text.length;
|
|
224
|
+
if (legTextChars <= MAX_LEG_TEXT_CHARS) legEvents.push(event);
|
|
225
|
+
} else if (event.type === "thinking_delta") {
|
|
226
|
+
legThinkingChars += event.thinking.length;
|
|
227
|
+
if (legThinkingChars <= MAX_LEG_THINKING_CHARS) legEvents.push(event);
|
|
228
|
+
} else if (
|
|
229
|
+
event.type === "thinking_signature"
|
|
230
|
+
|| event.type === "redacted_thinking"
|
|
231
|
+
) {
|
|
232
|
+
legEvents.push(event);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
yield event;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const advisorCalls = held.filter(call => call.name === ADVISOR_TOOL_NAME && call.closed);
|
|
239
|
+
const shouldIntercept = terminalConsumed
|
|
240
|
+
&& terminalEvent?.stopReason !== "max_tokens"
|
|
241
|
+
&& terminalEvent?.stopReason !== "content_filter"
|
|
242
|
+
&& advisorCalls.length > 0
|
|
243
|
+
&& !hasRealToolCall;
|
|
244
|
+
|
|
245
|
+
if (!shouldIntercept) {
|
|
246
|
+
// Plain leg: surface the terminal with merged usage from any earlier intercepted legs.
|
|
247
|
+
if (terminalEvent) {
|
|
248
|
+
const usage = mergeUsage(accumulatedUsage, terminalEvent.usage);
|
|
249
|
+
yield usage ? { ...terminalEvent, usage } : terminalEvent;
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Consume this leg's done — the continuation replaces it.
|
|
255
|
+
accumulatedUsage = mergeUsage(accumulatedUsage, terminalEvent?.usage);
|
|
256
|
+
if (continuations >= MAX_ADVISOR_CONTINUATIONS_PER_REQUEST || finalContinuationUsed) {
|
|
257
|
+
yield {
|
|
258
|
+
type: "error",
|
|
259
|
+
status: 502,
|
|
260
|
+
errorType: "advisor_continuation_limit",
|
|
261
|
+
message: "Advisor worker continuation limit reached; no further worker calls were sent.",
|
|
262
|
+
...(accumulatedUsage ? { usage: accumulatedUsage } : {}),
|
|
263
|
+
};
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
// A repeated call after tool removal gets one last paired limit result, never a loop.
|
|
267
|
+
const isFinalContinuation = consultations >= maxConsultations;
|
|
268
|
+
// Reserve before any consultation await, so simultaneous guard entries share the bound.
|
|
269
|
+
continuations += 1;
|
|
270
|
+
finalContinuationUsed ||= isFinalContinuation;
|
|
271
|
+
const timestamp = Date.now();
|
|
272
|
+
const assistant = assistantMessageFromLeg(legEvents, advisorCalls, timestamp);
|
|
273
|
+
const messages: OcxMessage[] = [...parsed.context.messages];
|
|
274
|
+
if (assistant) messages.push(assistant);
|
|
275
|
+
|
|
276
|
+
for (const call of advisorCalls) {
|
|
277
|
+
if (consultations >= maxConsultations) {
|
|
278
|
+
// Runtime-owned limit text: not advice, and never a genuine advice wrapper.
|
|
279
|
+
messages.push(advisorToolResult(call, {
|
|
280
|
+
ok: false,
|
|
281
|
+
isError: true,
|
|
282
|
+
content: plan.formatUnavailable("limit", "consultation limit reached for this request"),
|
|
283
|
+
}, timestamp));
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
consultations += 1;
|
|
287
|
+
const args = parseAdvisorArgs(call.argsBuf);
|
|
288
|
+
// Later calls in the same leg must see what came before them: the rebuilt worker turn
|
|
289
|
+
// and every earlier advisor result in this leg's accumulated messages.
|
|
290
|
+
const consultParsed: OcxParsedRequest = {
|
|
291
|
+
...parsed,
|
|
292
|
+
context: { ...parsed.context, messages },
|
|
293
|
+
};
|
|
294
|
+
let outcome: AdvisorConsultOutcome;
|
|
295
|
+
try {
|
|
296
|
+
outcome = await plan.consult(consultParsed, "manual", args.question);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
// Runtime-owned failure text only: the implementation neutralizes untrusted exception
|
|
299
|
+
// text so it can never forge a genuine advice wrapper.
|
|
300
|
+
outcome = {
|
|
301
|
+
ok: false,
|
|
302
|
+
isError: true,
|
|
303
|
+
content: plan.formatUnavailable("manual", error instanceof Error ? error.message : String(error)),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
messages.push(advisorToolResult(call, outcome, timestamp));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const exhausted = consultations >= maxConsultations;
|
|
310
|
+
const tools = exhausted
|
|
311
|
+
? parsed.context.tools?.filter(tool => !tool.advisor && tool.name !== ADVISOR_TOOL_NAME)
|
|
312
|
+
: parsed.context.tools;
|
|
313
|
+
const choice = parsed.options.toolChoice;
|
|
314
|
+
const nextParsed: OcxParsedRequest = {
|
|
315
|
+
...parsed,
|
|
316
|
+
context: { ...parsed.context, messages, tools },
|
|
317
|
+
options: exhausted && ((typeof choice === "object" && choice !== null && "name" in choice && choice.name === ADVISOR_TOOL_NAME)
|
|
318
|
+
|| (choice === "required" && !tools?.length))
|
|
319
|
+
? { ...parsed.options, toolChoice: "auto" }
|
|
320
|
+
: parsed.options,
|
|
321
|
+
};
|
|
322
|
+
parsed = nextParsed;
|
|
323
|
+
yield { type: "assistant_boundary" };
|
|
324
|
+
try {
|
|
325
|
+
source = await options.continuation(parsed);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
yield {
|
|
328
|
+
type: "error",
|
|
329
|
+
message: error instanceof Error ? error.message : String(error),
|
|
330
|
+
...(accumulatedUsage ? { usage: accumulatedUsage } : {}),
|
|
331
|
+
};
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
@@ -77,13 +77,104 @@ interface RenderedTool { name: string; raw: string }
|
|
|
77
77
|
/** The declaration fence codex-rs opens: three backticks and "ts". */
|
|
78
78
|
const DECLARATION_FENCE_SHAPE: Fence = { char: "`", length: 3 };
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
const DECLARATION_PREFIX = "declare const tools: { ";
|
|
81
|
+
const PROMISE_SEPARATOR = "): Promise<";
|
|
82
|
+
const DECLARATION_SUFFIX = ">; };";
|
|
83
|
+
// Bounds keep a caller-controlled description linear work: a rendered listing is a heading, a
|
|
84
|
+
// description, and a small declaration per tool, so anything this large cannot be the codex-rs
|
|
85
|
+
// shape this parser exists to recognize.
|
|
86
|
+
const MAX_DESCRIPTION_LENGTH = 1 << 20;
|
|
87
|
+
const MAX_DECLARATION_LENGTH = 64 * 1024;
|
|
88
|
+
|
|
89
|
+
const isSpace = (ch: string | undefined): boolean => ch === undefined || /\s/.test(ch);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The declared tool name when body is a complete declaration as render_code_mode_sample emits it,
|
|
93
|
+
* single- or multi-line, else undefined. Fixed boundaries plus one forward scan that balances the
|
|
94
|
+
* parameter list's brackets and the return type's generics: no backtracking, so a fence body full
|
|
95
|
+
* of candidate separators stays linear work.
|
|
96
|
+
*/
|
|
97
|
+
function completeDeclarationName(body: readonly string[]): string | undefined {
|
|
82
98
|
const text = body.join("\n");
|
|
83
|
-
|
|
84
|
-
if (!text.startsWith(
|
|
99
|
+
if (text.length > MAX_DECLARATION_LENGTH) return undefined;
|
|
100
|
+
if (!text.startsWith(DECLARATION_PREFIX) || !text.endsWith(DECLARATION_SUFFIX)) return undefined;
|
|
101
|
+
const signature = text.slice(DECLARATION_PREFIX.length, -DECLARATION_SUFFIX.length);
|
|
102
|
+
const openParen = signature.indexOf("(");
|
|
103
|
+
const name = openParen > 0 ? signature.slice(0, openParen) : "";
|
|
104
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name)) return undefined;
|
|
85
105
|
// A named, typed parameter (`args` for functions, `input` for freeform) and a non-empty return type.
|
|
86
|
-
|
|
106
|
+
const rest = signature.slice(openParen + 1);
|
|
107
|
+
const typed = rest.startsWith("args: ") ? 6 : rest.startsWith("input: ") ? 7 : -1;
|
|
108
|
+
if (typed < 0 || isSpace(rest[typed])) return undefined;
|
|
109
|
+
// Quoted text and comments are opaque to delimiter matching. Each skip advances
|
|
110
|
+
// past its text once; an unterminated region cannot contain a closing declaration.
|
|
111
|
+
const isOpaqueTypeText = (at: number): boolean => {
|
|
112
|
+
const ch = rest[at];
|
|
113
|
+
return ch === '"' || ch === "'" || ch === "`"
|
|
114
|
+
|| ch === "/" && (rest[at + 1] === "/" || rest[at + 1] === "*");
|
|
115
|
+
};
|
|
116
|
+
const skipTypeText = (at: number): number | undefined => {
|
|
117
|
+
const quote = rest[at]!;
|
|
118
|
+
if (quote === "/" && rest[at + 1] === "/") {
|
|
119
|
+
let i = at + 2;
|
|
120
|
+
while (i < rest.length && rest[i] !== "\n" && rest[i] !== "\r"
|
|
121
|
+
&& rest[i] !== "\u2028" && rest[i] !== "\u2029") i += 1;
|
|
122
|
+
return i < rest.length ? i : undefined;
|
|
123
|
+
}
|
|
124
|
+
if (quote === "/" && rest[at + 1] === "*") {
|
|
125
|
+
const end = rest.indexOf("*/", at + 2);
|
|
126
|
+
return end < 0 ? undefined : end + 1;
|
|
127
|
+
}
|
|
128
|
+
let i = at + 1;
|
|
129
|
+
while (i < rest.length) {
|
|
130
|
+
const ch = rest[i]!;
|
|
131
|
+
if (ch === "\\") i += 2;
|
|
132
|
+
else if (ch === quote) return i;
|
|
133
|
+
else i += 1;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
};
|
|
137
|
+
// The parameter list ends at the ')' that balances the opening '(': a ')' nested inside the
|
|
138
|
+
// parameter's own type (e.g. `args: (a: string) => Promise<void>`) cannot pose as the separator.
|
|
139
|
+
// The type must also say something before that ')': `args: )` or `args: /* note */)` names no type.
|
|
140
|
+
let depth = 1;
|
|
141
|
+
let close = -1;
|
|
142
|
+
let typeContent = false;
|
|
143
|
+
for (let at = typed; at < rest.length; at += 1) {
|
|
144
|
+
const ch = rest[at]!;
|
|
145
|
+
if (isOpaqueTypeText(at)) {
|
|
146
|
+
if (ch !== "/") typeContent = true; // a quoted literal is a type, a comment is not
|
|
147
|
+
const end = skipTypeText(at);
|
|
148
|
+
if (end === undefined) return undefined;
|
|
149
|
+
at = end;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (ch === "(" || ch === "[" || ch === "{") depth += 1;
|
|
153
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth -= 1;
|
|
154
|
+
if (depth === 0) { close = at; break; }
|
|
155
|
+
if (!isSpace(ch)) typeContent = true;
|
|
156
|
+
}
|
|
157
|
+
if (close < 0 || !typeContent || !rest.startsWith(PROMISE_SEPARATOR, close)) return undefined;
|
|
158
|
+
const head = rest[close + PROMISE_SEPARATOR.length];
|
|
159
|
+
if (head === ">" || isSpace(head)) return undefined;
|
|
160
|
+
// The return type runs to the stripped suffix, which already supplied exactly one '>' for the
|
|
161
|
+
// Promise's '<': generic depth must stay positive to the end and land at exactly 1 — a '>' that
|
|
162
|
+
// reaches zero ('Promise<unknown>; garbage') is surplus text, a depth left above 1
|
|
163
|
+
// ('Promise<Map<string, unknown') never closed its own generic, and a '=>' arrow never counts.
|
|
164
|
+
let generic = 1;
|
|
165
|
+
for (let at = close + PROMISE_SEPARATOR.length; at < rest.length; at += 1) {
|
|
166
|
+
const ch = rest[at]!;
|
|
167
|
+
if (isOpaqueTypeText(at)) {
|
|
168
|
+
const end = skipTypeText(at);
|
|
169
|
+
if (end === undefined) return undefined;
|
|
170
|
+
at = end;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (ch === "<") generic += 1;
|
|
174
|
+
else if (ch === ">" && rest[at - 1] !== "=") generic -= 1;
|
|
175
|
+
if (generic === 0) return undefined;
|
|
176
|
+
}
|
|
177
|
+
return generic === 1 ? name : undefined;
|
|
87
178
|
}
|
|
88
179
|
|
|
89
180
|
/** codex-rs normalize_code_mode_identifier: invalid identifier characters become `_`. */
|
|
@@ -114,9 +205,21 @@ function renderedHeading(match: RegExpExecArray): RenderedTool | undefined {
|
|
|
114
205
|
* alone. Lines inside a quoted fence can neither open a section nor declare a tool.
|
|
115
206
|
*/
|
|
116
207
|
function renderedNestedTools(description: string): RenderedTool[] {
|
|
208
|
+
if (description.length > MAX_DESCRIPTION_LENGTH) return [];
|
|
117
209
|
const lines = description.split("\n");
|
|
118
210
|
const rendered: RenderedTool[] = [];
|
|
119
211
|
const pending: RenderedTool[] = [];
|
|
212
|
+
// Ascending indexes into `pending` per name, so a declaration finds its owner in one lookup
|
|
213
|
+
// instead of rescanning every pending heading.
|
|
214
|
+
const pendingByName = new Map<string, number[]>();
|
|
215
|
+
const dropPendingTo = (length: number): void => {
|
|
216
|
+
while (pending.length > length) {
|
|
217
|
+
const removed = pending.pop()!;
|
|
218
|
+
const indexes = pendingByName.get(removed.name)!;
|
|
219
|
+
indexes.pop();
|
|
220
|
+
if (indexes.length === 0) pendingByName.delete(removed.name);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
120
223
|
let fence: Fence | undefined;
|
|
121
224
|
for (let i = 0; i < lines.length; i += 1) {
|
|
122
225
|
const line = lines[i]!;
|
|
@@ -127,7 +230,11 @@ function renderedNestedTools(description: string): RenderedTool[] {
|
|
|
127
230
|
const match = TOOL_HEADING.exec(line);
|
|
128
231
|
if (match) {
|
|
129
232
|
const heading = renderedHeading(match);
|
|
130
|
-
if (heading)
|
|
233
|
+
if (heading) {
|
|
234
|
+
const indexes = pendingByName.get(heading.name);
|
|
235
|
+
if (indexes) indexes.push(pending.length); else pendingByName.set(heading.name, [pending.length]);
|
|
236
|
+
pending.push(heading);
|
|
237
|
+
}
|
|
131
238
|
continue;
|
|
132
239
|
}
|
|
133
240
|
if (line === DECLARATION_LEAD && lines[i + 1] === DECLARATION_FENCE) {
|
|
@@ -137,10 +244,11 @@ function renderedNestedTools(description: string): RenderedTool[] {
|
|
|
137
244
|
const body = lines.slice(i + 2, close);
|
|
138
245
|
// The outermost pending heading with the declared name owns it, so a same-name heading quoted
|
|
139
246
|
// inside the tool's own description cannot take over the genuine section.
|
|
140
|
-
const
|
|
247
|
+
const declared = completeDeclarationName(body);
|
|
248
|
+
const owner = declared === undefined ? -1 : pendingByName.get(declared)?.[0] ?? -1;
|
|
141
249
|
if (owner >= 0) {
|
|
142
250
|
rendered.push(pending[owner]!);
|
|
143
|
-
|
|
251
|
+
dropPendingTo(owner);
|
|
144
252
|
}
|
|
145
253
|
i = close;
|
|
146
254
|
continue;
|
|
@@ -16,6 +16,7 @@ import { resolveClientRetryAfter } from "../../lib/retry-after";
|
|
|
16
16
|
import { formatErrorResponse } from "../../bridge";
|
|
17
17
|
import { OLLAMA_LATE_ATTRIBUTION_LIMIT_MESSAGE } from "../../adapters/ollama-native";
|
|
18
18
|
import { isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
19
|
+
import { MODEL_NOT_ALLOWED_FOR_KEY } from "../admission-model-scope";
|
|
19
20
|
import { usageFromResponsesPayload } from "../request-log";
|
|
20
21
|
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
21
22
|
|
|
@@ -49,6 +50,9 @@ export async function consumeComboFailure(
|
|
|
49
50
|
// display-safe body carries a recognized quota message.
|
|
50
51
|
let quotaConfirmedByBody = false;
|
|
51
52
|
const serverError = response.status >= 500 && response.status < 600;
|
|
53
|
+
// A local key-scope refusal is already the client's answer: rebuilding it as an upstream_error
|
|
54
|
+
// would hide the stable model_not_allowed_for_key type clients branch on.
|
|
55
|
+
let scopeRefusalBody: string | undefined;
|
|
52
56
|
try {
|
|
53
57
|
const body = await readBoundedResponseBody(response, { signal, reportUtf8Validity: serverError });
|
|
54
58
|
// A 5xx body counts as quota or classification evidence only when it decoded as valid
|
|
@@ -71,6 +75,9 @@ export async function consumeComboFailure(
|
|
|
71
75
|
upstreamMessage = normalized.message;
|
|
72
76
|
upstreamType = normalized.type;
|
|
73
77
|
}
|
|
78
|
+
if (nonReplayable && response.status === 403 && upstreamType === MODEL_NOT_ALLOWED_FOR_KEY) {
|
|
79
|
+
scopeRefusalBody = body.text;
|
|
80
|
+
}
|
|
74
81
|
}
|
|
75
82
|
} catch (error) {
|
|
76
83
|
if (signal?.aborted) throw error;
|
|
@@ -106,7 +113,9 @@ export async function consumeComboFailure(
|
|
|
106
113
|
now,
|
|
107
114
|
includeDefault: false,
|
|
108
115
|
});
|
|
109
|
-
const failureResponse =
|
|
116
|
+
const failureResponse = scopeRefusalBody !== undefined
|
|
117
|
+
? new Response(scopeRefusalBody, { status: 403, headers: { "Content-Type": "application/json" } })
|
|
118
|
+
: formatErrorResponse(
|
|
110
119
|
response.status,
|
|
111
120
|
cyberFailure ? (upstreamType ?? CYBER_POLICY_ERROR_CODE) : "upstream_error",
|
|
112
121
|
message,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { effectiveAzureOutputBudget } from "../../adapters/azure";
|
|
2
|
+
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
1
3
|
import type { CodexAccountModelRefusal } from "../../combos/failover";
|
|
2
4
|
import {
|
|
3
5
|
isCodexReasoningEffort,
|
|
@@ -712,12 +714,31 @@ export async function executeComboResponses(
|
|
|
712
714
|
retryAfter: String(Math.max(1, Math.ceil(remainingQuotaCooldownMs / 1000))),
|
|
713
715
|
}) : lastFailure;
|
|
714
716
|
};
|
|
717
|
+
// The caller's output ceiling, captured once so a floor applied to one target never
|
|
718
|
+
// leaks into the next. An established parent estimate wins; otherwise (combo dispatch
|
|
719
|
+
// precedes request preparation) the caller's own positive cap is reserved.
|
|
720
|
+
const callerBudget = isPlainObject(body) ? body.max_output_tokens : undefined;
|
|
721
|
+
const callerOutputCeilingTokens = logCtx.spendOutputCeilingTokens
|
|
722
|
+
?? (typeof callerBudget === "number" && Number.isFinite(callerBudget) && callerBudget > 0
|
|
723
|
+
? Math.trunc(callerBudget) : undefined);
|
|
715
724
|
while (pick) {
|
|
716
725
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
717
726
|
const firstComboTarget = comboTargetsDispatched === 0;
|
|
718
727
|
const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
|
|
719
728
|
// The inherited spend tracker observes this parent log, before the child has its own label.
|
|
720
729
|
logCtx.spendPoolId = targetRoute.providerName;
|
|
730
|
+
// Combo prepays before its child normalizes the route; the parent tracker must
|
|
731
|
+
// already see this target's effective cap rather than a previous target's cap.
|
|
732
|
+
const spendProvider = resolveWireProtocolOverride(targetRoute.providerName, targetRoute.modelId,
|
|
733
|
+
targetRoute.provider, options.inboundWire ?? "responses", targetRoute.staticPolicy);
|
|
734
|
+
const outputBudget = effectiveAzureOutputBudget(spendProvider, callerBudget);
|
|
735
|
+
if (outputBudget !== callerBudget && typeof outputBudget === "number" && outputBudget > 0) {
|
|
736
|
+
logCtx.spendOutputCeilingTokens = Math.trunc(outputBudget);
|
|
737
|
+
} else if (callerOutputCeilingTokens === undefined) {
|
|
738
|
+
delete logCtx.spendOutputCeilingTokens;
|
|
739
|
+
} else {
|
|
740
|
+
logCtx.spendOutputCeilingTokens = callerOutputCeilingTokens;
|
|
741
|
+
}
|
|
721
742
|
// Derive the target's allowance before booking its initial send; the booking occupies it.
|
|
722
743
|
const targetSendBudget = comboSendScope
|
|
723
744
|
? comboTargetSendBudget(comboSendScope, combo.targets.length - 1 - comboTargetsDispatched)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
2
2
|
import { finalizeTranslatorBudgetResponse } from "../../lib/translator-budget";
|
|
3
|
+
import { isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
3
4
|
import {
|
|
4
5
|
isNativePassthroughSseResponse,
|
|
5
6
|
markNativePassthroughSseResponse,
|
|
@@ -67,6 +68,7 @@ export function finalizeAccountLease(response: Response, release: () => void): R
|
|
|
67
68
|
async cancel(reason) { try { await reader.cancel(reason); } finally { finish(); } },
|
|
68
69
|
});
|
|
69
70
|
const wrapped = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
71
|
+
if (isNonReplayableResponse(response)) markResponseNonReplayable(wrapped);
|
|
70
72
|
if (isNativePassthroughSseResponse(response)) markNativePassthroughSseResponse(wrapped);
|
|
71
73
|
if (isEagerRelaySseResponse(response)) markEagerRelaySseResponse(wrapped);
|
|
72
74
|
if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(wrapped);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { effectiveAzureOutputBudget } from "../../adapters/azure";
|
|
1
2
|
import { sanitizeLogMetadataString } from "../../lib/redact";
|
|
2
3
|
import type { RouteResult } from "../../router";
|
|
3
4
|
import type { InboundWire } from "../../providers/registry";
|
|
@@ -211,6 +212,16 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
211
212
|
logCtx.model = virtualModel?.selectedModelId ?? route.modelId;
|
|
212
213
|
logCtx.provider = route.providerName;
|
|
213
214
|
logCtx.spendPoolId = route.providerName;
|
|
215
|
+
// Admission precedes adapter serialization. Reserve the same explicit output cap
|
|
216
|
+
// the final destination will send, and recompute from the caller on every fallback.
|
|
217
|
+
const rawBudget = parsed._rawBody && typeof parsed._rawBody === "object"
|
|
218
|
+
? (parsed._rawBody as { max_output_tokens?: unknown }).max_output_tokens : undefined;
|
|
219
|
+
const outputBudget = effectiveAzureOutputBudget(route.provider,
|
|
220
|
+
route.provider.adapter === "azure" || route.provider.adapter === "azure-openai"
|
|
221
|
+
? rawBudget : parsed.options.maxOutputTokens);
|
|
222
|
+
if (typeof outputBudget === "number" && outputBudget > 0) {
|
|
223
|
+
logCtx.spendOutputCeilingTokens = Math.trunc(outputBudget);
|
|
224
|
+
}
|
|
214
225
|
logCtx.providerAdapter = route.provider.adapter;
|
|
215
226
|
logCtx.routeDecision = route.routeDecision;
|
|
216
227
|
logCtx.policyEligibility = route.policyEligibility;
|