@bitkyc08/opencodex 2.75.0 → 2.76.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 +6 -1
- package/bin/ocx.mjs +12 -1
- package/gui/dist/assets/App-CddorHdA.js +51 -0
- package/gui/dist/assets/App-DMudtISW.css +1 -0
- package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
- package/gui/dist/assets/index-Btze1I3T.css +1 -0
- package/gui/dist/assets/index-CvnY7E6i.js +86 -0
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/opengateway.svg +12 -0
- package/package.json +1 -1
- package/src/adapters/anthropic/account-metadata.ts +17 -0
- package/src/adapters/anthropic/client-identity.ts +69 -0
- package/src/adapters/anthropic/passthrough.ts +23 -4
- package/src/adapters/anthropic.ts +18 -1
- package/src/adapters/command-code-project-context.ts +7 -11
- package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
- package/src/adapters/kiro-retry.ts +23 -8
- package/src/adapters/openai-chat/messages.ts +1 -1
- package/src/adapters/registry.ts +8 -1
- package/src/adapters/zed.ts +377 -0
- package/src/bridge/sse.ts +4 -2
- package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
- package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
- package/src/chatgpt/app-server-shim/filter.ts +157 -0
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
- package/src/chatgpt/app-server-shim/launcher.ts +91 -0
- package/src/claude/desktop-3p-startup.ts +62 -0
- package/src/claude/desktop-3p.ts +5 -0
- package/src/claude/first-party-settings.ts +14 -0
- package/src/claude/intercept/cli-catalog.ts +150 -0
- package/src/claude/intercept/cli-picker.ts +82 -0
- package/src/claude/intercept/listener.ts +21 -6
- package/src/claude/intercept/picker-bootstrap.ts +23 -5
- package/src/claude/intercept/picker-models.ts +57 -13
- package/src/claude/intercept/runtime.ts +30 -2
- package/src/claude/intercept/settings.ts +3 -0
- package/src/cli/agent.ts +172 -0
- package/src/cli/alias.ts +2 -1
- package/src/cli/capabilities-command.ts +3 -2
- package/src/cli/capabilities.ts +87 -0
- package/src/cli/chatgpt-command.ts +140 -0
- package/src/cli/combo.ts +144 -19
- package/src/cli/config-command.ts +3 -1
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +5 -24
- package/src/cli/init.ts +16 -2
- package/src/cli/integrations.ts +13 -0
- package/src/cli/internal-command.ts +12 -5
- package/src/cli/observe.ts +6 -1
- package/src/cli/registry.ts +6 -0
- package/src/cli/route-policy.ts +3 -1
- package/src/cli/runtime-api.ts +5 -2
- package/src/cli/status-oauth.ts +5 -0
- package/src/cli/status.ts +8 -1
- package/src/cli/stop-approval.ts +55 -16
- package/src/cli/stop-report.ts +5 -0
- package/src/clients/lazycodex.ts +48 -0
- package/src/clients/omo-role-models.ts +111 -0
- package/src/codex/account-credit-use.ts +99 -0
- package/src/codex/account-lifecycle.ts +2 -0
- package/src/codex/agent-role-models.ts +171 -0
- package/src/codex/auth-api/account-list.ts +10 -3
- package/src/codex/auth-api/routes.ts +34 -0
- package/src/codex/auth-context.ts +52 -11
- package/src/codex/catalog/discovered-natives.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +6 -1
- package/src/codex/catalog/model-hints.ts +9 -1
- package/src/codex/catalog/provider-models.ts +60 -0
- package/src/codex/catalog/routed-gather.ts +30 -5
- package/src/codex/convergence-types.ts +2 -1
- package/src/codex/main-account-external-usage.ts +62 -0
- package/src/codex/main-account-hard-lock.ts +24 -13
- package/src/codex/native-profile-processes.ts +53 -16
- package/src/codex/prompt-layers/toml-edit.ts +1 -1
- package/src/codex/quota-types.ts +3 -7
- package/src/codex/quota.ts +19 -4
- package/src/codex/role-auto-assign.ts +159 -0
- package/src/codex/role-sizing-limits.ts +2 -0
- package/src/codex/role-sizing.ts +216 -0
- package/src/codex/routing/selection.ts +36 -0
- package/src/codex/routing/thread-affinity.ts +2 -0
- package/src/codex/routing.ts +4 -4
- package/src/codex/subagent-model-fallback.ts +82 -26
- package/src/codex/subagent-selectable-models.ts +32 -0
- package/src/combos/index.ts +18 -0
- package/src/combos/jev-decision-contract.ts +20 -0
- package/src/combos/jev-dispatch.ts +18 -0
- package/src/combos/jev-model-backend.ts +127 -0
- package/src/combos/jev.ts +249 -33
- package/src/combos/types.ts +125 -2
- package/src/config/diagnostics.ts +18 -0
- package/src/config/macos-system-proxy.ts +72 -15
- package/src/config/owner-registry.ts +13 -4
- package/src/config/process-state.ts +17 -8
- package/src/config/proxy-env.ts +35 -5
- package/src/config/schema/config-schema.ts +30 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config/serving-runtimes.ts +95 -1
- package/src/generated/compatibility-version.json +336 -156
- package/src/lib/errors.ts +7 -7
- package/src/lib/jsonc.ts +13 -0
- package/src/lib/local-chat-completion.ts +144 -0
- package/src/lib/provider-outbound.ts +87 -1
- package/src/lib/provider-runtime-fetch.ts +23 -0
- package/src/lib/provider-tls-profile.ts +221 -0
- package/src/lib/request-execution-budget.ts +16 -6
- package/src/lib/spend-reservation-ledger.ts +54 -20
- package/src/lib/sse-decoder.ts +20 -17
- package/src/lib/synced-state-location.ts +96 -0
- package/src/lib/winsw.ts +14 -5
- package/src/oauth/anthropic-account-refusal.ts +53 -4
- package/src/oauth/anthropic-model-quota.ts +83 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
- package/src/oauth/anthropic-routing.ts +112 -58
- package/src/oauth/health.ts +39 -3
- package/src/oauth/index.ts +14 -0
- package/src/oauth/kiro-account-load.ts +10 -0
- package/src/oauth/zed.ts +134 -0
- package/src/providers/model-discovery.ts +19 -8
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/provider-id-rewrite.ts +9 -2
- package/src/providers/quota/account-cache.ts +29 -7
- package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
- package/src/providers/quota/anthropic-family-headers.ts +25 -0
- package/src/providers/quota/vendor-probes-oauth.ts +5 -1
- package/src/providers/quota-types.ts +4 -0
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +55 -0
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/xai-fast-model.ts +12 -4
- package/src/providers/zed.ts +598 -0
- package/src/responses/state/snapshot-select.ts +22 -9
- package/src/responses/state.ts +2 -3
- package/src/router.ts +9 -0
- package/src/server/anthropic-rate-limit-headers.ts +7 -0
- package/src/server/auth-cors.ts +7 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/claude-messages.ts +59 -21
- package/src/server/index/claude-intercept-lifecycle.ts +71 -25
- package/src/server/index/optional-listeners.ts +4 -0
- package/src/server/index/spend-ledger-lifecycle.ts +4 -0
- package/src/server/management/agent-settings-routes.ts +74 -37
- package/src/server/management/claude-desktop-picker-routes.ts +7 -0
- package/src/server/management/claude-intercept-routes.ts +35 -0
- package/src/server/management/codex-agent-role-routes.ts +122 -0
- package/src/server/management/codex-role-auto-assign.ts +208 -0
- package/src/server/management/combo-routes.ts +62 -5
- package/src/server/management/config-routes.ts +35 -3
- package/src/server/management/context.ts +4 -1
- package/src/server/management/decision-discovery.ts +74 -0
- package/src/server/management/decision-model-validation.ts +80 -0
- package/src/server/management/decision-routes.ts +160 -0
- package/src/server/management/native-integration-routes.ts +16 -0
- package/src/server/management/provider-routes.ts +30 -34
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management/zed-provider-probe.ts +38 -0
- package/src/server/management-api.ts +12 -1
- package/src/server/messages-native-oauth.ts +7 -2
- package/src/server/messages-native.ts +7 -3
- package/src/server/relay.ts +3 -1
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +18 -3
- package/src/server/responses/compact.ts +14 -1
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-combo.ts +67 -11
- package/src/server/responses/core-normalize.ts +21 -1
- package/src/server/responses/core-options.ts +11 -1
- package/src/server/responses/core.ts +5 -3
- package/src/server/responses/fetch-helpers.ts +6 -3
- package/src/server/responses/jev-model-invoke.ts +190 -0
- package/src/server/responses/passthrough-delivery.ts +16 -0
- package/src/server/responses/policy-fallback.ts +55 -3
- package/src/server/responses/policy-request-scope.ts +80 -0
- package/src/server/responses/request-prepare.ts +70 -18
- package/src/server/responses/request-transport.ts +54 -23
- package/src/server/responses/run-turn-execution.ts +1 -0
- package/src/server/responses/sidecar-execution.ts +9 -3
- package/src/server/responses-hosted-image-display.ts +225 -0
- package/src/service/managing-cli.ts +34 -4
- package/src/service/systemd-env.ts +75 -0
- package/src/service/windows-guarded-manager.ts +60 -19
- package/src/service/windows-scheduler.ts +22 -3
- package/src/service/windows-taskxml.ts +32 -10
- package/src/service-manager-probe.ts +72 -19
- package/src/types/config.ts +31 -1
- package/src/types/provider.ts +2 -0
- package/src/types/request.ts +2 -0
- package/src/update/index.ts +39 -6
- package/src/update/job.ts +63 -63
- package/src/update/npm-cache-preflight.d.mts +33 -1
- package/src/update/npm-cache-preflight.mjs +131 -18
- package/src/update/ownership-transaction.ts +19 -1
- package/src/update/restart-ownership.ts +80 -6
- package/src/update/transactional-install.d.mts +2 -0
- package/src/update/transactional-install.mjs +6 -1
- package/src/usage/jev-stats.ts +28 -1
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +5 -3
- package/src/vision/index.ts +1 -0
- package/src/vision/plan.ts +2 -1
- package/src/vision/routed-describe.ts +29 -121
- package/src/web-search/alpha-search.ts +3 -3
- package/src/web-search/anthropic-executor.ts +6 -4
- package/src/web-search/index.ts +1 -1
- package/src/web-search/loop.ts +3 -3
- package/src/web-search/passthrough-bridge.ts +2 -1
- package/src/web-search/run-turn-loop.ts +1 -1
- package/src/web-search/sidecar-providers.ts +2 -1
- package/gui/dist/assets/App-ia4U0ufY.css +0 -1
- package/gui/dist/assets/App-xm0zGNOe.js +0 -51
- package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
- package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
- package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { readBoundedResponseBytes } from "../../lib/bounded-body";
|
|
2
|
+
import { JevModelInvokeError, type JevModelInvoke, type JevModelInvokeResult } from "../../combos/jev-model-backend";
|
|
3
|
+
import { JEV_MAX_REQUEST_BYTES, JEV_MAX_RESPONSE_BYTES } from "../../combos/jev";
|
|
4
|
+
import type { OcxConfig } from "../../types";
|
|
5
|
+
import { codexEffortRank, isCodexReasoningEffort } from "../../reasoning-effort";
|
|
6
|
+
import { routeConcreteModel } from "../../router";
|
|
7
|
+
import { supportedLadderFor } from "../effort-policy";
|
|
8
|
+
import { createInferenceSendBudget } from "../inference/context";
|
|
9
|
+
import { tryAdmitTurn } from "../lifecycle";
|
|
10
|
+
import type { RequestLogContext } from "../request-log";
|
|
11
|
+
import { createSseInspector } from "../relay";
|
|
12
|
+
import type { HandleResponsesOptions } from "./core-options";
|
|
13
|
+
|
|
14
|
+
type HandleResponses = (
|
|
15
|
+
req: Request,
|
|
16
|
+
config: OcxConfig,
|
|
17
|
+
logCtx: RequestLogContext,
|
|
18
|
+
options?: HandleResponsesOptions,
|
|
19
|
+
) => Promise<Response>;
|
|
20
|
+
|
|
21
|
+
export interface JevModelInvokerContext {
|
|
22
|
+
/** The request that owns the combo; only its spend root is reused, never its headers. */
|
|
23
|
+
req: Pick<Request, "headers">;
|
|
24
|
+
config: OcxConfig;
|
|
25
|
+
/** Parent options: only the typed admission scope and Codex auth policy cross over. */
|
|
26
|
+
options: Pick<HandleResponsesOptions, "admission" | "codexAuthPolicy">;
|
|
27
|
+
handleResponses: HandleResponses;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
31
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Concatenate assistant message text from a Responses output array. */
|
|
35
|
+
function messageOutputText(output: unknown): string | undefined {
|
|
36
|
+
if (!Array.isArray(output)) return undefined;
|
|
37
|
+
const parts: string[] = [];
|
|
38
|
+
for (const item of output) {
|
|
39
|
+
if (!isRecord(item) || item.type !== "message" || !Array.isArray(item.content)) continue;
|
|
40
|
+
for (const part of item.content) {
|
|
41
|
+
if (isRecord(part) && part.type === "output_text" && typeof part.text === "string") parts.push(part.text);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return parts.length > 0 ? parts.join("") : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function responseUsage(response: Record<string, unknown>): Record<string, number> | undefined {
|
|
48
|
+
if (!isRecord(response.usage)) return undefined;
|
|
49
|
+
const usage: Record<string, number> = {};
|
|
50
|
+
for (const key of ["input_tokens", "output_tokens"] as const) {
|
|
51
|
+
const value = response.usage[key];
|
|
52
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) usage[key] = value;
|
|
53
|
+
}
|
|
54
|
+
return Object.keys(usage).length > 0 ? usage : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function terminalResult(response: Record<string, unknown>, deltaText: string): JevModelInvokeResult {
|
|
58
|
+
if (response.error !== undefined && response.error !== null) throw new JevModelInvokeError("http", "decision model failed");
|
|
59
|
+
if (response.status !== "completed") throw new JevModelInvokeError("malformed", "decision model did not complete");
|
|
60
|
+
const text = messageOutputText(response.output) ?? deltaText;
|
|
61
|
+
const usage = responseUsage(response);
|
|
62
|
+
return { text, ...(usage ? { usage } : {}) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function decodeSse(bytes: Uint8Array): JevModelInvokeResult {
|
|
66
|
+
let completed: Record<string, unknown> | undefined;
|
|
67
|
+
let terminal: string | undefined;
|
|
68
|
+
let deltaText = "";
|
|
69
|
+
const inspector = createSseInspector({
|
|
70
|
+
onTerminal: status => { terminal ??= status; },
|
|
71
|
+
onCompletedResponse: value => { if (isRecord(value)) completed = value; },
|
|
72
|
+
onParsedPayload: payload => {
|
|
73
|
+
if (isRecord(payload) && payload.type === "response.output_text.delta" && typeof payload.delta === "string") {
|
|
74
|
+
deltaText += payload.delta;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
try {
|
|
79
|
+
inspector.feed(bytes);
|
|
80
|
+
inspector.finish();
|
|
81
|
+
} finally {
|
|
82
|
+
inspector.dispose();
|
|
83
|
+
}
|
|
84
|
+
if (terminal !== "completed" || !completed) {
|
|
85
|
+
throw new JevModelInvokeError(terminal === "failed" ? "http" : "malformed", "decision model did not complete");
|
|
86
|
+
}
|
|
87
|
+
return terminalResult({ ...completed, status: completed.status ?? "completed" }, deltaText);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The answer is one short JSON object, so the decision turn carries its own output ceiling. It
|
|
92
|
+
* bounds billed output (including reasoning) and gives the spend reservation a nonzero ceiling;
|
|
93
|
+
* a truncated answer is malformed and fails open like any other bad reply.
|
|
94
|
+
*/
|
|
95
|
+
export const JEV_MODEL_MAX_OUTPUT_TOKENS = 1024;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The cheapest reasoning effort the decision model declares, so a reasoning model cannot spend
|
|
99
|
+
* the whole output ceiling thinking before it writes the JSON answer. A model with no known
|
|
100
|
+
* ladder gets no reasoning field at all: some OpenAI-compatible servers reject one they do not
|
|
101
|
+
* support, and an unknown capability is not evidence that the field is safe to send.
|
|
102
|
+
*/
|
|
103
|
+
export function jevDecisionReasoningEffort(config: OcxConfig, model: string): string | undefined {
|
|
104
|
+
let ladder: string[] | undefined;
|
|
105
|
+
try {
|
|
106
|
+
const route = routeConcreteModel(config, model);
|
|
107
|
+
ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
|
|
108
|
+
} catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const rankable = (ladder ?? []).filter(isCodexReasoningEffort);
|
|
112
|
+
if (rankable.length === 0) return undefined;
|
|
113
|
+
if (rankable.includes("low")) return "low";
|
|
114
|
+
return rankable.reduce((lowest, effort) => (codexEffortRank(effort) < codexEffortRank(lowest) ? effort : lowest));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Build the invoker that runs one decision prompt as an internal Responses turn through the
|
|
119
|
+
* normal router. The decision request is its own logical request: a fresh turn lease and send
|
|
120
|
+
* budget, a detached log context whose spend tracker is settled here, and no caller credential
|
|
121
|
+
* or conversation header. It therefore runs on credentials configured on provider rows only.
|
|
122
|
+
*/
|
|
123
|
+
export function createJevModelInvoker(context: JevModelInvokerContext): JevModelInvoke {
|
|
124
|
+
return async ({ model, instructions, input, signal }) => {
|
|
125
|
+
const effort = jevDecisionReasoningEffort(context.config, model);
|
|
126
|
+
const body = JSON.stringify({
|
|
127
|
+
model,
|
|
128
|
+
stream: true,
|
|
129
|
+
store: false,
|
|
130
|
+
instructions,
|
|
131
|
+
input: [{ role: "user", content: [{ type: "input_text", text: input }] }],
|
|
132
|
+
tools: [],
|
|
133
|
+
max_output_tokens: JEV_MODEL_MAX_OUTPUT_TOKENS,
|
|
134
|
+
...(effort ? { reasoning: { effort } } : {}),
|
|
135
|
+
});
|
|
136
|
+
if (new TextEncoder().encode(body).byteLength > JEV_MAX_REQUEST_BYTES) {
|
|
137
|
+
throw new JevModelInvokeError("malformed", "decision request too large");
|
|
138
|
+
}
|
|
139
|
+
const lease = tryAdmitTurn();
|
|
140
|
+
if (!lease) throw new JevModelInvokeError("network", "decision turn not admitted");
|
|
141
|
+
const childLog: RequestLogContext = { model, provider: "unknown", inboundProtocol: "responses" };
|
|
142
|
+
let usage: Record<string, number> | undefined;
|
|
143
|
+
try {
|
|
144
|
+
const request = new Request("http://localhost/v1/responses", {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: { "content-type": "application/json" },
|
|
147
|
+
body,
|
|
148
|
+
signal,
|
|
149
|
+
});
|
|
150
|
+
const response = await context.handleResponses(request, context.config, childLog, {
|
|
151
|
+
abortSignal: signal,
|
|
152
|
+
...(context.options.admission ? { admission: context.options.admission } : {}),
|
|
153
|
+
...(context.options.codexAuthPolicy ? { codexAuthPolicy: context.options.codexAuthPolicy } : {}),
|
|
154
|
+
// Explicit nulls: handleResponses captures caller credentials only when these are undefined.
|
|
155
|
+
callerDirectAuth: null,
|
|
156
|
+
openAiSidecarAuth: null,
|
|
157
|
+
nativeCallerAuth: null,
|
|
158
|
+
sendBudget: createInferenceSendBudget(context.req, childLog),
|
|
159
|
+
turnAdmissionLease: lease,
|
|
160
|
+
internalDecisionCall: true,
|
|
161
|
+
});
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
try { void response.body?.cancel().catch(() => undefined); } catch { /* best effort */ }
|
|
164
|
+
throw new JevModelInvokeError("http", `decision model returned ${response.status}`);
|
|
165
|
+
}
|
|
166
|
+
const bounded = await readBoundedResponseBytes(response, { maxBytes: JEV_MAX_RESPONSE_BYTES, signal });
|
|
167
|
+
if (bounded.oversized) throw new JevModelInvokeError("malformed", "decision response too large");
|
|
168
|
+
const result = response.headers.get("content-type")?.includes("text/event-stream")
|
|
169
|
+
? decodeSse(bounded.bytes)
|
|
170
|
+
: (() => {
|
|
171
|
+
let parsed: unknown;
|
|
172
|
+
try {
|
|
173
|
+
parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bounded.bytes));
|
|
174
|
+
} catch {
|
|
175
|
+
throw new JevModelInvokeError("malformed", "decision response is not JSON");
|
|
176
|
+
}
|
|
177
|
+
if (!isRecord(parsed)) throw new JevModelInvokeError("malformed", "decision response is not an object");
|
|
178
|
+
return terminalResult(parsed, "");
|
|
179
|
+
})();
|
|
180
|
+
usage = result.usage;
|
|
181
|
+
return result;
|
|
182
|
+
} finally {
|
|
183
|
+
// No final log row is written for this detached context, so settle its spend here.
|
|
184
|
+
childLog.spendTracker?.settle(childLog.usage ?? (usage
|
|
185
|
+
? { inputTokens: usage.input_tokens ?? 0, outputTokens: usage.output_tokens ?? 0 }
|
|
186
|
+
: undefined));
|
|
187
|
+
lease.release();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
PLAINTEXT_V2_AGENT_MESSAGE_RESTORE_OVERFLOW_MESSAGE,
|
|
100
100
|
} from "../../responses/plaintext-v2-agent-messages";
|
|
101
101
|
import { createResponsesFieldBackfillBlockRewrite } from "./responses-field-backfill";
|
|
102
|
+
import { createHostedImageDisplayRewrite, isLocalCodexImageClient } from "../responses-hosted-image-display";
|
|
102
103
|
import { createResponsesFunctionToolRepairBlockRewrite } from "../responses-function-tool-repair";
|
|
103
104
|
import {
|
|
104
105
|
createUndeclaredToolCallGuardBlockRewrite,
|
|
@@ -840,6 +841,10 @@ export async function deliverPassthroughResponse(
|
|
|
840
841
|
: undefined,
|
|
841
842
|
createTerminalErrorRedactionBlockRewrite(nativeExchange.request.headers, maskCredential),
|
|
842
843
|
rememberPlaintextBlock,
|
|
844
|
+
// Display-only projection must follow every continuation-cache observer.
|
|
845
|
+
isLocalCodexImageClient(req.headers, options.admission?.kind, options.inboundWire)
|
|
846
|
+
? createHostedImageDisplayRewrite()
|
|
847
|
+
: undefined,
|
|
843
848
|
].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
|
|
844
849
|
const clientBlockRewrite = blockRewrites.length > 0
|
|
845
850
|
? composeSseBlockRewrites(...blockRewrites)
|
|
@@ -1318,6 +1323,17 @@ export async function deliverPassthroughResponse(
|
|
|
1318
1323
|
JSON.parse(grokUpstreamEchoEnabled ? clientJson : text) as { id?: unknown; output?: unknown; status?: unknown; model?: unknown },
|
|
1319
1324
|
);
|
|
1320
1325
|
} catch { /* non-JSON despite content-type; recording is best-effort */ }
|
|
1326
|
+
if (isLocalCodexImageClient(req.headers, options.admission?.kind, options.inboundWire)) {
|
|
1327
|
+
const imageDisplay = createHostedImageDisplayRewrite();
|
|
1328
|
+
try {
|
|
1329
|
+
clientJson = imageDisplay.json(clientJson);
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
if (error instanceof RangeError) {
|
|
1332
|
+
return formatErrorResponse(502, "upstream_error", "hosted image result count exceeds local display limit");
|
|
1333
|
+
}
|
|
1334
|
+
throw error;
|
|
1335
|
+
} finally { imageDisplay.dispose?.(); }
|
|
1336
|
+
}
|
|
1321
1337
|
// #875: the transport-neutral reliability policy forced a bounded JSON
|
|
1322
1338
|
// upstream for a client that asked for SSE. Reframe the completed JSON
|
|
1323
1339
|
// as the canonical terminal SSE sequence (created → output_item.done →
|
|
@@ -11,6 +11,8 @@ import { captureExplicitOpenAiCallerAuth } from "../../providers/openai-sidecar"
|
|
|
11
11
|
import { captureCallerDirectAuth } from "../../providers/caller-authorization";
|
|
12
12
|
import { resolvePolicyProfileId } from "../../routing/profile";
|
|
13
13
|
import { parseSyntheticRowId } from "../fast-row";
|
|
14
|
+
import { routeConcreteModel } from "../../router";
|
|
15
|
+
import { isPolicyCandidateRefusal, policyWireDestination, type PolicyRequestScope } from "./policy-request-scope";
|
|
14
16
|
|
|
15
17
|
type CoreHandler = typeof handleResponsesCore;
|
|
16
18
|
type CoreOptions = Parameters<CoreHandler>[3];
|
|
@@ -23,6 +25,25 @@ function candidateKey(candidate: Pick<RouteCandidateTrace, "provider" | "model">
|
|
|
23
25
|
return `${candidate.provider}\u0000${candidate.model}`;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
function staysWithinPolicyAfterRedirect(
|
|
29
|
+
config: OcxConfig,
|
|
30
|
+
policyEligibility: ReadonlySet<string>,
|
|
31
|
+
candidate: RouteCandidateTrace,
|
|
32
|
+
triedDestinations: ReadonlySet<string>,
|
|
33
|
+
): boolean {
|
|
34
|
+
try {
|
|
35
|
+
// Both inspection and execution resolve the original concrete candidate, without public
|
|
36
|
+
// combo/profile aliases or combo selection writes. A redirect must remain in the evaluation.
|
|
37
|
+
const routed = routeConcreteModel(config, `${candidate.provider}/${candidate.model}`);
|
|
38
|
+
return policyEligibility.has(`${routed.providerName}\u0000${routed.modelId}`)
|
|
39
|
+
&& !triedDestinations.has(policyWireDestination(routed));
|
|
40
|
+
} catch {
|
|
41
|
+
// A route that fails to resolve (e.g. a redirect cycle) cannot succeed on retry; its terminal
|
|
42
|
+
// error is rarely hop-worthy, so keep the fallback alive for the next eligible candidate.
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
/**
|
|
27
48
|
* Rank the remaining candidates from the ORIGINAL policy trace. The initial
|
|
28
49
|
* decision stays immutable; fallback execution belongs in attempts[], not in a
|
|
@@ -87,6 +108,7 @@ async function shouldHopPolicyCandidate(response: Response, signal?: AbortSignal
|
|
|
87
108
|
if (response.status < 400 || signal?.aborted) return false;
|
|
88
109
|
// A response that must not be sent again cannot open a policy-candidate retry either.
|
|
89
110
|
if (isNonReplayableResponse(response)) return false;
|
|
111
|
+
if (isPolicyCandidateRefusal(response)) return true;
|
|
90
112
|
try {
|
|
91
113
|
const inspected = await readBoundedResponseBody(response.clone(), { signal });
|
|
92
114
|
const text = inspected.displaySafe ? inspected.text : "";
|
|
@@ -131,11 +153,13 @@ export async function handleResponsesWithPolicyFallback(
|
|
|
131
153
|
deps: PolicyFallbackDeps = {},
|
|
132
154
|
): Promise<Response> {
|
|
133
155
|
const runCore = deps.runCore ?? handleResponsesCore;
|
|
156
|
+
const policyScope: PolicyRequestScope = options.policyRequestScope ?? { triedDestinations: new Set() };
|
|
134
157
|
let requestBodyReadNotified = false;
|
|
135
158
|
let storedPool401ReplayDispatched = false;
|
|
136
159
|
let rawBody: Record<string, unknown> | null = null;
|
|
137
160
|
const coreOptions: CoreOptions = {
|
|
138
161
|
...options,
|
|
162
|
+
policyRequestScope: policyScope,
|
|
139
163
|
openAiSidecarAuth: options.openAiSidecarAuth === undefined
|
|
140
164
|
? captureExplicitOpenAiCallerAuth(req.headers, config) : options.openAiSidecarAuth,
|
|
141
165
|
nativeCallerAuth: options.nativeCallerAuth === undefined
|
|
@@ -175,25 +199,52 @@ export async function handleResponsesWithPolicyFallback(
|
|
|
175
199
|
if (overload) return overload;
|
|
176
200
|
throw error;
|
|
177
201
|
}
|
|
178
|
-
const initialTrace = logCtx.routeDecision;
|
|
202
|
+
const initialTrace = policyScope.decision ?? logCtx.routeDecision;
|
|
179
203
|
const initialRequestedModel = logCtx.requestedModel;
|
|
180
204
|
if (!rawBody || !isPolicyDecision(initialTrace)) return response;
|
|
181
205
|
|
|
182
206
|
const tried = new Set<string>([
|
|
183
207
|
candidateKey({ provider: initialTrace.selected.provider, model: initialTrace.selected.model }),
|
|
184
208
|
]);
|
|
209
|
+
const selectedCandidate = initialTrace.selected.candidateIndex === undefined ? undefined
|
|
210
|
+
: initialTrace.candidates[initialTrace.selected.candidateIndex];
|
|
211
|
+
if (selectedCandidate) tried.add(candidateKey(selectedCandidate));
|
|
212
|
+
// Redirect eligibility needs the full evaluation membership, not the bounded trace list.
|
|
213
|
+
const policyEligibility: ReadonlySet<string> = policyScope.eligibility ?? logCtx.policyEligibility
|
|
214
|
+
?? new Set(initialTrace.candidates
|
|
215
|
+
.filter(candidate => candidate.eligible)
|
|
216
|
+
.map(candidate => candidateKey(candidate)));
|
|
217
|
+
policyScope.eligibility = policyEligibility;
|
|
218
|
+
policyScope.decision = initialTrace;
|
|
219
|
+
policyScope.triedDestinations.add(policyScope.preparedDestination ?? policyWireDestination({
|
|
220
|
+
providerName: initialTrace.selected.provider, modelId: initialTrace.selected.model,
|
|
221
|
+
}));
|
|
185
222
|
|
|
186
223
|
while (!storedPool401ReplayDispatched && await shouldHopPolicyCandidate(response, req.signal)) {
|
|
187
224
|
if (req.signal.aborted) return response;
|
|
188
|
-
const next = rankPolicyFallbackCandidates(initialTrace, tried)
|
|
225
|
+
const next = rankPolicyFallbackCandidates(initialTrace, tried)
|
|
226
|
+
.find(candidate => staysWithinPolicyAfterRedirect(config, policyEligibility, candidate, policyScope.triedDestinations));
|
|
189
227
|
if (!next) return response;
|
|
190
228
|
tried.add(candidateKey(next));
|
|
191
229
|
|
|
230
|
+
// Retain the failed response's log owner until a candidate actually replaces it. This is
|
|
231
|
+
// deliberately shallow: completed attempts and the live spend tracker keep their identity.
|
|
232
|
+
const failureLog = { ...logCtx };
|
|
192
233
|
finishFailedPolicyAttempt(logCtx, response.status);
|
|
193
234
|
const retryRequest = requestWithCandidate(req, rawBody, next);
|
|
235
|
+
delete policyScope.preparedDestination;
|
|
194
236
|
try {
|
|
195
237
|
try {
|
|
196
|
-
|
|
238
|
+
const nextResponse = await runCore(retryRequest, config, logCtx, { ...coreOptions, policyFallbackCandidate: next });
|
|
239
|
+
// A locally skipped route owns neither the returned failure nor its usage/settlement.
|
|
240
|
+
// Preparation can replace route metadata and the tracker, or add fields absent before it.
|
|
241
|
+
if (isPolicyCandidateRefusal(nextResponse)) {
|
|
242
|
+
for (const key of Object.keys(logCtx)) {
|
|
243
|
+
if (!Object.hasOwn(failureLog, key)) Reflect.deleteProperty(logCtx, key);
|
|
244
|
+
}
|
|
245
|
+
Object.assign(logCtx, failureLog);
|
|
246
|
+
} else response = nextResponse;
|
|
247
|
+
if (policyScope.preparedDestination) policyScope.triedDestinations.add(policyScope.preparedDestination);
|
|
197
248
|
} catch (error) {
|
|
198
249
|
const overload = requestPacingOverloadResponse(error);
|
|
199
250
|
if (overload) return overload;
|
|
@@ -202,6 +253,7 @@ export async function handleResponsesWithPolicyFallback(
|
|
|
202
253
|
} finally {
|
|
203
254
|
logCtx.requestedModel = initialRequestedModel;
|
|
204
255
|
logCtx.routeDecision = initialTrace;
|
|
256
|
+
logCtx.policyEligibility = policyEligibility;
|
|
205
257
|
}
|
|
206
258
|
}
|
|
207
259
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { formatErrorResponse } from "../../bridge";
|
|
2
|
+
import type { RouteResult } from "../../router";
|
|
3
|
+
import type { RouteDecisionTraceV1 } from "../../routing/trace";
|
|
4
|
+
import { resolveOpenAiVirtualModel } from "../../providers/openai-virtual-models";
|
|
5
|
+
|
|
6
|
+
/** Request-owned authorization, separate from the diagnostic trace and each physical attempt. */
|
|
7
|
+
export interface PolicyRequestScope {
|
|
8
|
+
decision?: RouteDecisionTraceV1;
|
|
9
|
+
eligibility?: ReadonlySet<string>;
|
|
10
|
+
readonly triedDestinations: Set<string>;
|
|
11
|
+
preparedDestination?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function policyDestinationKey(provider: string, model: string): string {
|
|
15
|
+
return `${provider}\u0000${model}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function policyWireDestination(route: Pick<RouteResult, "providerName" | "modelId">): string {
|
|
19
|
+
const model = resolveOpenAiVirtualModel(route.providerName, route.modelId)?.wireModelId ?? route.modelId;
|
|
20
|
+
return policyDestinationKey(route.providerName, model);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class PolicyCandidateUnavailableError extends Error {
|
|
24
|
+
constructor() {
|
|
25
|
+
super("The candidate cannot serve this request within its original routing policy.");
|
|
26
|
+
this.name = "PolicyCandidateUnavailableError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const localRefusals = new WeakSet<Response>();
|
|
31
|
+
|
|
32
|
+
export function policyCandidateRefusalResponse(error: unknown): Response | undefined {
|
|
33
|
+
if (!(error instanceof PolicyCandidateUnavailableError)) return undefined;
|
|
34
|
+
const response = formatErrorResponse(409, "policy_candidate_unavailable", error.message);
|
|
35
|
+
localRefusals.add(response);
|
|
36
|
+
return response;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Upstream text or a matching error code cannot authorize another policy attempt. */
|
|
40
|
+
export function isPolicyCandidateRefusal(response: Response): boolean {
|
|
41
|
+
return localRefusals.has(response);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function capturePolicyRequestRoute(scope: PolicyRequestScope, route: RouteResult): RouteResult {
|
|
45
|
+
if (!scope.eligibility && route.policyEligibility && route.routeDecision?.routeKind === "policy") {
|
|
46
|
+
scope.eligibility = new Set(route.policyEligibility);
|
|
47
|
+
scope.decision = route.routeDecision;
|
|
48
|
+
}
|
|
49
|
+
if (!scope.eligibility) return route;
|
|
50
|
+
if (!scope.eligibility.has(policyDestinationKey(route.providerName, route.modelId))) {
|
|
51
|
+
throw new PolicyCandidateUnavailableError();
|
|
52
|
+
}
|
|
53
|
+
// Preparation and normalization both project from RouteResult into the log. Keep the original
|
|
54
|
+
// authorization and decision on every allowed replacement, including recovery's second pass.
|
|
55
|
+
route.routeKind = "policy";
|
|
56
|
+
route.policyEligibility = scope.eligibility;
|
|
57
|
+
route.routeDecision = scope.decision;
|
|
58
|
+
return route;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Only the admitted selector's existing virtual-model rewrite may change its final route id. */
|
|
62
|
+
export function assertPolicyNormalizedRoute(
|
|
63
|
+
scope: PolicyRequestScope,
|
|
64
|
+
admitted: Pick<RouteResult, "providerName" | "modelId">,
|
|
65
|
+
route: RouteResult,
|
|
66
|
+
): void {
|
|
67
|
+
if (!scope.eligibility) return;
|
|
68
|
+
if (route.providerName !== admitted.providerName || (route.modelId !== admitted.modelId
|
|
69
|
+
&& route.modelId !== resolveOpenAiVirtualModel(admitted.providerName, admitted.modelId)?.wireModelId)) {
|
|
70
|
+
throw new PolicyCandidateUnavailableError();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Called once preparation has settled the wire, before dispatch; account retries keep their own budget. */
|
|
75
|
+
export function recordPolicyPreparedDestination(scope: PolicyRequestScope, provider: string, model: string): void {
|
|
76
|
+
if (!scope.eligibility) return;
|
|
77
|
+
const destination = policyDestinationKey(provider, model);
|
|
78
|
+
if (scope.triedDestinations.has(destination)) throw new PolicyCandidateUnavailableError();
|
|
79
|
+
scope.preparedDestination = destination;
|
|
80
|
+
}
|