@bitkyc08/opencodex 2.65.0 → 2.66.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 +14 -0
- package/gui/dist/assets/App-DyHlFeNB.js +51 -0
- package/gui/dist/assets/App-gkoJIEOV.css +1 -0
- package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-CI8cVKWu.js} +1 -1
- package/gui/dist/assets/index-BiwnVAUm.js +86 -0
- package/gui/dist/assets/index-ComVsdSk.css +1 -0
- package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-_WyYzbMt.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +80 -0
- package/src/adapters/anthropic/passthrough.ts +221 -0
- package/src/adapters/anthropic-image-codec.ts +56 -19
- package/src/adapters/anthropic-image-normalize.ts +20 -10
- package/src/adapters/anthropic.ts +55 -24
- package/src/adapters/coding-agent/protocol.ts +29 -19
- package/src/adapters/cursor/live-transport.ts +18 -3
- package/src/adapters/cursor/native-exec-shell.ts +18 -63
- package/src/adapters/cursor/native-exec.ts +5 -2
- package/src/adapters/cursor/native-foreground-shell.ts +44 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -1
- package/src/adapters/exec-tool-result-normalize.ts +4 -1
- package/src/adapters/google.ts +12 -0
- package/src/adapters/ollama-native.ts +32 -1
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
- package/src/adapters/openai-chat.ts +6 -4
- package/src/adapters/openai-responses/passthrough.ts +1 -0
- package/src/adapters/openai-responses/reasoning.ts +45 -1
- package/src/bridge/response-json.ts +10 -2
- package/src/chat/outbound.ts +104 -52
- package/src/claude/claude-code-block.ts +16 -0
- package/src/claude/context-windows.ts +52 -4
- package/src/claude/desktop-first-party.ts +25 -8
- package/src/claude/inbound.ts +22 -0
- package/src/claude/intercept/connect-proxy.ts +37 -4
- package/src/claude/intercept/proxy-auth.ts +166 -0
- package/src/claude/intercept/runtime.ts +21 -0
- package/src/claude/intercept/settings.ts +33 -12
- package/src/claude/outbound.ts +45 -27
- package/src/cli/account-extended.ts +47 -15
- package/src/cli/account-target.ts +115 -0
- package/src/cli/account.ts +20 -28
- package/src/cli/agent.ts +10 -1
- package/src/cli/api-protocols.ts +206 -0
- package/src/cli/capabilities.ts +86 -0
- package/src/cli/combo.ts +2 -2
- package/src/cli/connect.ts +127 -3
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/doctor.ts +33 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/link.ts +288 -0
- package/src/cli/registry.ts +23 -0
- package/src/cli/runtime-api.ts +79 -0
- package/src/cli/system-command.ts +21 -2
- package/src/client/connect.ts +125 -23
- package/src/client/hub-relay.ts +12 -8
- package/src/client/link-join.ts +323 -0
- package/src/client/link-relay.ts +241 -0
- package/src/client/link-state.ts +110 -0
- package/src/client/link-teardown.ts +63 -0
- package/src/client/link-tunnel.ts +465 -0
- package/src/client/machine-listener.ts +14 -5
- package/src/client/runtime.ts +91 -42
- package/src/client/state.ts +4 -0
- package/src/codex/catalog/build-entries.ts +4 -0
- package/src/codex/catalog/derive-entry.ts +5 -0
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +4 -1
- package/src/codex/desktop-switches.ts +88 -8
- package/src/codex/inject/plan.ts +16 -3
- package/src/codex/inject.ts +3 -0
- package/src/codex/internal/catalog-writer.ts +13 -2
- package/src/combos/failover.ts +3 -2
- package/src/combos/index.ts +12 -0
- package/src/combos/jev.ts +646 -0
- package/src/combos/request.ts +5 -0
- package/src/combos/resolve.ts +17 -15
- package/src/combos/types.ts +47 -3
- package/src/config/atomic-write.ts +3 -0
- package/src/config/live-reconcile.ts +20 -4
- package/src/config/persist-unlocked.ts +20 -7
- package/src/config/schema/config-schema.ts +16 -0
- package/src/config/schema/leaf-validators.ts +38 -1
- package/src/generated/compatibility-version.json +416 -128
- package/src/integrations/omp-yaml-source.ts +109 -4
- package/src/lab/automation/orchestrator.ts +60 -4
- package/src/lab/events/limits.ts +1 -1
- package/src/lib/bounded-body.ts +55 -38
- package/src/lib/config-ownership.ts +11 -22
- package/src/lib/local-desktop-snapshot-capability.ts +56 -0
- package/src/lib/local-management-capability.ts +25 -3
- package/src/lib/optional-shutdown-hooks.ts +17 -0
- package/src/lib/state-store-registrations.ts +2 -2
- package/src/lib/windows-elevation.ts +62 -25
- package/src/lib/windows-secret-acl.ts +114 -23
- package/src/link/admission-wait.ts +73 -0
- package/src/link/compensation.ts +100 -0
- package/src/link/fingerprint.ts +21 -0
- package/src/link/paths.ts +19 -0
- package/src/link/ports.ts +12 -0
- package/src/link/routes.ts +32 -0
- package/src/link/ssh-argv.ts +170 -0
- package/src/link/ssh-config.ts +152 -0
- package/src/link/ssh-runner.ts +161 -0
- package/src/link/status-projection.ts +96 -0
- package/src/link/store.ts +139 -0
- package/src/link/supervisor.ts +404 -0
- package/src/link/tunnel-state.ts +91 -0
- package/src/protocols/baseline.ts +88 -0
- package/src/protocols/codecs/chat.ts +17 -0
- package/src/protocols/codecs/messages.ts +18 -0
- package/src/protocols/codecs/responses.ts +17 -0
- package/src/protocols/contract.ts +185 -0
- package/src/protocols/dto.ts +267 -0
- package/src/protocols/encoders/adapter-events.ts +876 -0
- package/src/protocols/encoders/chat.ts +243 -0
- package/src/protocols/encoders/messages.ts +462 -0
- package/src/protocols/envelope.ts +43 -0
- package/src/protocols/features.ts +295 -0
- package/src/protocols/guard.ts +33 -0
- package/src/protocols/opaque-state.ts +132 -0
- package/src/protocols/path.ts +39 -0
- package/src/protocols/plan-snapshot.ts +226 -0
- package/src/protocols/plan.ts +192 -0
- package/src/protocols/provider-summary.ts +101 -0
- package/src/protocols/settings.ts +118 -0
- package/src/protocols/shadow-plan.ts +36 -0
- package/src/protocols/shadow.ts +62 -0
- package/src/protocols/trace.ts +335 -0
- package/src/providers/model-rename-migration.ts +3 -3
- package/src/providers/registry/entries-core.ts +6 -1
- package/src/providers/registry/entries-extended.ts +15 -0
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/types.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/remote-control/workspace-hub.ts +5 -4
- package/src/responses/apply-patch-envelope.ts +6 -1
- package/src/responses/code-mode-shell-input.ts +4 -2
- package/src/responses/freeform-wrapper-scan.ts +51 -24
- package/src/responses/progressive-freeform-input.ts +14 -20
- package/src/responses/spill-store.ts +187 -33
- package/src/responses/state/snapshot-select.ts +49 -0
- package/src/responses/state/spill-inspect.ts +46 -0
- package/src/responses/state/spill-queue.ts +16 -0
- package/src/responses/state.ts +51 -27
- package/src/server/audio-client.ts +8 -3
- package/src/server/audio-upstream.ts +9 -4
- package/src/server/auth-cors.ts +25 -8
- package/src/server/chat-completions.ts +71 -11
- package/src/server/chat-native-eligibility.ts +74 -0
- package/src/server/chat-native.ts +139 -74
- package/src/server/claude-messages.ts +140 -22
- package/src/server/hub-usage.ts +4 -3
- package/src/server/index/link-listener.ts +195 -0
- package/src/server/index/optional-listeners.ts +115 -0
- package/src/server/index/serve-options.ts +44 -10
- package/src/server/index.ts +10 -9
- package/src/server/inference/attempt.ts +51 -0
- package/src/server/inference/client-encoder-delivery.ts +235 -0
- package/src/server/inference/client-wire-log.ts +49 -0
- package/src/server/inference/client-wire.ts +69 -0
- package/src/server/inference/context.ts +15 -0
- package/src/server/inference/final-log.ts +37 -0
- package/src/server/local-desktop-snapshot-auth.ts +57 -0
- package/src/server/management/agent-settings-routes.ts +15 -13
- package/src/server/management/api-access.ts +11 -1
- package/src/server/management/config-routes.ts +3 -6
- package/src/server/management/context.ts +25 -0
- package/src/server/management/link-routes.ts +535 -0
- package/src/server/management/logs-usage-routes.ts +31 -1
- package/src/server/management/native-integration-routes.ts +6 -11
- package/src/server/management/oauth-account-routes.ts +42 -16
- package/src/server/management/protocol-routes.ts +159 -0
- package/src/server/management/protocol-settings-patch.ts +145 -0
- package/src/server/management/provider-patch-transaction.ts +61 -0
- package/src/server/management/provider-routes.ts +39 -18
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sidebar-routes.ts +8 -3
- package/src/server/management/system-routes.ts +18 -1
- package/src/server/management/usage-aggregate-cache.ts +206 -6
- package/src/server/management-api.ts +26 -1
- package/src/server/management-auth.ts +19 -5
- package/src/server/messages-native-eligibility.ts +161 -0
- package/src/server/messages-native-oauth.ts +149 -0
- package/src/server/messages-native.ts +790 -0
- package/src/server/relay.ts +9 -0
- package/src/server/request-log-filter.ts +92 -0
- package/src/server/request-log.ts +31 -70
- package/src/server/responses/adapter-delivery.ts +43 -16
- package/src/server/responses/agent-task-recovery.ts +90 -22
- package/src/server/responses/codex-ws-exchange.ts +4 -0
- package/src/server/responses/codex-ws-wire.ts +10 -1
- package/src/server/responses/core-combo-native.ts +323 -0
- package/src/server/responses/core-combo.ts +224 -13
- package/src/server/responses/core-options.ts +22 -0
- package/src/server/responses/core.ts +14 -26
- package/src/server/responses/request-sidecar-auth.ts +8 -3
- package/src/server/responses/run-turn-execution.ts +257 -63
- package/src/server/responses/sidecar-execution.ts +7 -4
- package/src/server/system-env.ts +7 -0
- package/src/service/diagnostics.ts +7 -1
- package/src/service/launchd.ts +26 -37
- package/src/service/windows-ops.ts +24 -20
- package/src/types/config.ts +58 -1
- package/src/types.ts +2 -0
- package/src/update/transactional-install.d.mts +0 -1
- package/src/update/transactional-install.mjs +17 -17
- package/src/usage/jev-stats.ts +495 -0
- package/src/usage/log.ts +24 -3
- package/src/web-search/run-turn-loop.ts +568 -0
- package/gui/dist/assets/App-8NMiZxT0.css +0 -1
- package/gui/dist/assets/App-DGLte6IR.js +0 -50
- package/gui/dist/assets/index--EWgGQvZ.css +0 -1
- package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildOpenAIChatPassthroughRequest, createOpenAIChatAdapter } from "../adapters/openai-chat";
|
|
2
|
-
import { chatBodyCarriesImage, chatBodyCarriesToolResultImage } from "../chat/image-parts";
|
|
3
2
|
import type { AdapterRequest, ProviderAdapter } from "../adapters/base";
|
|
3
|
+
import { isNativeChatRouteEligible } from "./chat-native-eligibility";
|
|
4
4
|
import {
|
|
5
5
|
chatCompletionsErrorBody,
|
|
6
6
|
chatCompletionsErrorResponse,
|
|
@@ -15,12 +15,13 @@ import {
|
|
|
15
15
|
CYBER_POLICY_ERROR_CODE,
|
|
16
16
|
isCyberPolicyCode,
|
|
17
17
|
isCyberPolicyMessage,
|
|
18
|
+
SEND_BUDGET_EXHAUSTED_CODE,
|
|
18
19
|
} from "../lib/errors";
|
|
20
|
+
import type { RequestExecutionBudget } from "../lib/request-execution-budget";
|
|
19
21
|
import type { AdmissionLease } from "../lib/admission";
|
|
20
22
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
21
23
|
import { redactSecretString } from "../lib/redact";
|
|
22
24
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
23
|
-
import { isModelTextOnly, requiresVisionPreprocessing } from "../vision";
|
|
24
25
|
import {
|
|
25
26
|
applyUpstreamRecoveryInit,
|
|
26
27
|
fetchWithResetRetry,
|
|
@@ -32,6 +33,8 @@ import {
|
|
|
32
33
|
REPLAY_REFUSAL_CLIENT_HEADERS,
|
|
33
34
|
REPLAY_REFUSED_STATUS,
|
|
34
35
|
retainReplayRefusal,
|
|
36
|
+
SendBudgetExhaustedError,
|
|
37
|
+
TRANSIENT_RETRY_MAX_ATTEMPTS,
|
|
35
38
|
UpstreamRetryEvidenceError,
|
|
36
39
|
type UpstreamSendRecovery,
|
|
37
40
|
UPSTREAM_RESET_REPLAY_REFUSED_CODE,
|
|
@@ -58,20 +61,23 @@ import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
|
58
61
|
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, sendWithConnectionPolicy } from "./responses/fetch-helpers";
|
|
59
62
|
import { linkAbortSignal } from "./responses";
|
|
60
63
|
import {
|
|
61
|
-
addFinalRequestLog,
|
|
62
|
-
beginRequestAttempt,
|
|
63
64
|
noteProviderAttemptSend,
|
|
64
65
|
recordKeyAttemptFailure,
|
|
65
66
|
recordKeyWireAttemptUsage,
|
|
66
67
|
recordFirstOutput,
|
|
67
68
|
recordAttemptCredentialSource,
|
|
68
|
-
sealRequestAttemptIdentity,
|
|
69
69
|
type RequestLogContext,
|
|
70
70
|
} from "./request-log";
|
|
71
71
|
import { jsonCompletionSse, nativeChatSse, structuredError, usageFromChat } from "./chat-native-sse";
|
|
72
|
+
import { beginInferenceAttempt, type InferenceAttempt } from "./inference/attempt";
|
|
73
|
+
import { createFinalRequestLog } from "./inference/final-log";
|
|
72
74
|
import { registerTurn, unregisterTurn } from "./lifecycle";
|
|
73
75
|
import { attachRequestSpendTracker } from "./responses/request-spend";
|
|
74
76
|
import { workflowRefusalResponse } from "./workflow-refusal";
|
|
77
|
+
import type { ComboProtocolSource } from "./responses/core-combo-native";
|
|
78
|
+
import type { ProtocolEnvelope } from "../protocols/envelope";
|
|
79
|
+
|
|
80
|
+
export { isNativeChatRouteEligible, nativeChatDeclineReason } from "./chat-native-eligibility";
|
|
75
81
|
|
|
76
82
|
type Rec = Record<string, unknown>;
|
|
77
83
|
|
|
@@ -154,50 +160,12 @@ function isRec(value: unknown): value is Rec {
|
|
|
154
160
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
155
161
|
}
|
|
156
162
|
|
|
157
|
-
export function isNativeChatRouteEligible(route: RouteResult, rawBody: Rec, config?: OcxConfig): boolean {
|
|
158
|
-
const provider = route.provider;
|
|
159
|
-
if (provider.adapter !== "openai-chat") return false;
|
|
160
|
-
if (provider.authMode !== undefined && provider.authMode !== "key" && provider.authMode !== "local") return false;
|
|
161
|
-
// Combo and policy execution own multi-candidate retries in the Responses pipeline.
|
|
162
|
-
if (route.combo || route.routeKind === "combo" || route.routeKind === "policy") return false;
|
|
163
|
-
if (rawBody.store === true || rawBody.background === true) return false;
|
|
164
|
-
if (typeof rawBody.previous_response_id === "string" && rawBody.previous_response_id.length > 0) return false;
|
|
165
|
-
if (rawBody.compaction_trigger !== undefined) return false;
|
|
166
|
-
// A standard Chat tool message accepts a string or text parts, not image_url, so
|
|
167
|
-
// normalizing a Pi/Anthropic tool image into image_url is not enough on its own —
|
|
168
|
-
// the part is still inside a tool message. The translated adapter already places
|
|
169
|
-
// tool-result images in a following user carrier after the complete paired batch
|
|
170
|
-
// (flushToolResultImages), so divert these requests there. Ordinary user images and
|
|
171
|
-
// text-only tool results keep the native fast path.
|
|
172
|
-
if (chatBodyCarriesToolResultImage(rawBody)) return false;
|
|
173
|
-
// Vision sidecar coverage (roadmap 180): a text-only routed model with an
|
|
174
|
-
// image-bearing body must go through the Responses pipeline, whose plan
|
|
175
|
-
// site describes or strips the image. The native fast path has no vision
|
|
176
|
-
// handling, so letting it keep such a request forwards raw pixels to a
|
|
177
|
-
// model the operator declared blind.
|
|
178
|
-
if (chatBodyCarriesImage(rawBody)) {
|
|
179
|
-
const needsVision = config
|
|
180
|
-
? requiresVisionPreprocessing(config, provider, route.modelId, route.providerName)
|
|
181
|
-
: isModelTextOnly(provider, route.modelId);
|
|
182
|
-
if (needsVision) return false;
|
|
183
|
-
}
|
|
184
|
-
if (Array.isArray(rawBody.tools)) {
|
|
185
|
-
for (const tool of rawBody.tools) {
|
|
186
|
-
if (!isRec(tool)) continue;
|
|
187
|
-
if (tool.type === "web_search" || tool.type === "web_search_preview" || tool.type === "image_generation") {
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
163
|
function chatCompletionJson(value: unknown): Rec | null {
|
|
196
164
|
if (!isRec(value) || !Array.isArray(value.choices) || value.choices.length === 0) return null;
|
|
197
165
|
return value;
|
|
198
166
|
}
|
|
199
167
|
|
|
200
|
-
interface HandleNativeChatOptions {
|
|
168
|
+
export interface HandleNativeChatOptions {
|
|
201
169
|
req: Request;
|
|
202
170
|
config: OcxConfig;
|
|
203
171
|
logCtx: RequestLogContext;
|
|
@@ -209,36 +177,108 @@ interface HandleNativeChatOptions {
|
|
|
209
177
|
translatorBudget: TranslatorBudget;
|
|
210
178
|
}
|
|
211
179
|
|
|
180
|
+
/** Records the outcome on whichever final row owns this attempt; the first call wins. */
|
|
181
|
+
export type NativeChatFinishLog = (
|
|
182
|
+
status: number,
|
|
183
|
+
message?: string,
|
|
184
|
+
closeReason?: "non_stream" | "terminal" | "client_cancel",
|
|
185
|
+
) => void;
|
|
186
|
+
|
|
187
|
+
export interface NativeChatExecution extends HandleNativeChatOptions {
|
|
188
|
+
finishLog: NativeChatFinishLog;
|
|
189
|
+
/**
|
|
190
|
+
* A combo child's per-target budget (PF-07). With it the attempt opens no spend tracker of its
|
|
191
|
+
* own: the combo's hop reservation already booked the first send on the request's shared
|
|
192
|
+
* counter, and every later physical send is reported to that counter, whose observer is the
|
|
193
|
+
* request's one spend tracker. A second tracker would book each send twice and, merged into
|
|
194
|
+
* the parent row, replace the one the final log settles.
|
|
195
|
+
*/
|
|
196
|
+
sendBudget?: RequestExecutionBudget;
|
|
197
|
+
/** Replaces the request-relative first-output mark; a combo child records its own. */
|
|
198
|
+
onFirstOutput?: () => void;
|
|
199
|
+
/** The lease a streamed body holds; defaults to `logIds.turnAdmissionLease`. */
|
|
200
|
+
turnAdmissionLease?: AdmissionLease;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The native Chat ingress: opens the attempt on the request's log context and owns the
|
|
205
|
+
* request's final log row, then runs the attempt.
|
|
206
|
+
*/
|
|
212
207
|
export async function handleNativeChatCompletions(options: HandleNativeChatOptions): Promise<Response> {
|
|
213
|
-
const {
|
|
208
|
+
const { logCtx, logIds, route } = options;
|
|
214
209
|
logCtx.inboundProtocol = "chat";
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
route.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
);
|
|
221
|
-
logCtx.activeAttempt = attempt;
|
|
222
|
-
logCtx.activeAttemptStartedAt = Date.now();
|
|
223
|
-
(logCtx.attempts ??= []).push(attempt);
|
|
224
|
-
sealRequestAttemptIdentity(attempt, route.providerName, "openai-chat", logCtx.accountLogLabel);
|
|
210
|
+
const attemptHandle = beginInferenceAttempt(logCtx, {
|
|
211
|
+
provider: route.providerName,
|
|
212
|
+
model: route.modelId,
|
|
213
|
+
adapter: "openai-chat",
|
|
214
|
+
});
|
|
215
|
+
attemptHandle.seal(logCtx.accountLogLabel);
|
|
225
216
|
|
|
226
|
-
|
|
227
|
-
const finishLog = (status
|
|
228
|
-
if (
|
|
229
|
-
|
|
217
|
+
const finalLog = createFinalRequestLog(logIds, logCtx);
|
|
218
|
+
const finishLog: NativeChatFinishLog = (status, message, closeReason = "non_stream") => {
|
|
219
|
+
if (finalLog.finished()) return;
|
|
220
|
+
// The failure text lands on the context before the row is written, and only once.
|
|
230
221
|
if (message) logCtx.upstreamError = redactSecretString(message).slice(0, 500);
|
|
231
|
-
|
|
222
|
+
finalLog.finish(status, { closeReason });
|
|
232
223
|
};
|
|
224
|
+
return runNativeChatAttempt({ ...options, finishLog }, attemptHandle);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The Chat source a combo hands its native children (PF-07). A child runs on the attempt the
|
|
229
|
+
* combo opened and reports through the combo's callbacks, so the final row stays the parent's.
|
|
230
|
+
*/
|
|
231
|
+
export function createNativeChatComboSource(input: {
|
|
232
|
+
req: Request;
|
|
233
|
+
config: OcxConfig;
|
|
234
|
+
envelope: ProtocolEnvelope;
|
|
235
|
+
requestedModel: string;
|
|
236
|
+
requestedStream: boolean;
|
|
237
|
+
translatorBudget: TranslatorBudget;
|
|
238
|
+
}): ComboProtocolSource {
|
|
239
|
+
return {
|
|
240
|
+
inbound: "chat",
|
|
241
|
+
envelope: input.envelope,
|
|
242
|
+
dispatchNativeChild: child => runNativeChatAttempt({
|
|
243
|
+
req: input.req,
|
|
244
|
+
config: input.config,
|
|
245
|
+
logCtx: child.childLog,
|
|
246
|
+
route: child.route,
|
|
247
|
+
chatBody: child.body,
|
|
248
|
+
requestedModel: input.requestedModel,
|
|
249
|
+
requestedStream: input.requestedStream,
|
|
250
|
+
translatorBudget: input.translatorBudget,
|
|
251
|
+
finishLog: child.finishLog,
|
|
252
|
+
sendBudget: child.sendBudget,
|
|
253
|
+
onFirstOutput: child.onFirstOutput,
|
|
254
|
+
...(child.turnAdmissionLease ? { turnAdmissionLease: child.turnAdmissionLease } : {}),
|
|
255
|
+
}, child.attemptHandle),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* One native Chat attempt on an already-open attempt row: effort normalization, the send
|
|
261
|
+
* loop with key failover and 429 replay, relay and usage. Every outcome is reported through
|
|
262
|
+
* `execution.finishLog`, so the caller decides which final row it lands on.
|
|
263
|
+
*/
|
|
264
|
+
export async function runNativeChatAttempt(
|
|
265
|
+
execution: NativeChatExecution,
|
|
266
|
+
attemptHandle: InferenceAttempt,
|
|
267
|
+
): Promise<Response> {
|
|
268
|
+
const { req, config, logCtx, logIds, route, requestedModel, requestedStream, translatorBudget, finishLog } = execution;
|
|
269
|
+
const { sendBudget } = execution;
|
|
270
|
+
const { attempt } = attemptHandle;
|
|
271
|
+
const onFirstOutput = execution.onFirstOutput
|
|
272
|
+
?? (logIds ? () => recordFirstOutput(logCtx, logIds.start) : undefined);
|
|
233
273
|
const fail = (status: number, message: string, type?: string, code?: string | null): Response => {
|
|
234
274
|
const safeMessage = redactSecretString(message);
|
|
235
275
|
finishLog(status, safeMessage);
|
|
236
276
|
return chatCompletionsErrorResponse(status, safeMessage, type, code);
|
|
237
277
|
};
|
|
238
278
|
|
|
239
|
-
normalizePinnedChatEffort(
|
|
240
|
-
logCtx.requestedServiceTier = typeof
|
|
241
|
-
?
|
|
279
|
+
normalizePinnedChatEffort(execution);
|
|
280
|
+
logCtx.requestedServiceTier = typeof execution.chatBody.service_tier === "string"
|
|
281
|
+
? execution.chatBody.service_tier
|
|
242
282
|
: undefined;
|
|
243
283
|
|
|
244
284
|
const upstream = new AbortController();
|
|
@@ -248,7 +288,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
248
288
|
// of adding another trackStreamLifetime wrapper (unsafe on bundled Bun#32111).
|
|
249
289
|
let streamTurnRegistered = false;
|
|
250
290
|
const transferTurnToStream = () => {
|
|
251
|
-
const lease = logIds?.turnAdmissionLease;
|
|
291
|
+
const lease = execution.turnAdmissionLease ?? logIds?.turnAdmissionLease;
|
|
252
292
|
if (!lease || typeof (lease as { bindAbortController?: unknown }).bindAbortController !== "function") return;
|
|
253
293
|
registerTurn(upstream, lease);
|
|
254
294
|
streamTurnRegistered = true;
|
|
@@ -269,7 +309,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
269
309
|
if (proactiveKeyProvider) route.provider = proactiveKeyProvider;
|
|
270
310
|
let activeProvider: OcxProviderConfig = route.provider;
|
|
271
311
|
stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
|
|
272
|
-
const spendTracker = attachRequestSpendTracker(req, logCtx);
|
|
312
|
+
const spendTracker = sendBudget ? undefined : attachRequestSpendTracker(req, logCtx);
|
|
273
313
|
let activeAdapter: ProviderAdapter = createOpenAIChatAdapter(activeProvider);
|
|
274
314
|
let activeRequest: AdapterRequest;
|
|
275
315
|
let retainedRequestBytes = 0;
|
|
@@ -287,7 +327,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
287
327
|
recordAttemptCredentialSource(attempt, route.providerName, activeProvider, activeAdapter.name);
|
|
288
328
|
return buildOpenAIChatPassthroughRequest(
|
|
289
329
|
activeProvider,
|
|
290
|
-
|
|
330
|
+
execution.chatBody,
|
|
291
331
|
route.modelId,
|
|
292
332
|
requestedStream,
|
|
293
333
|
fastPolicyForModel(activeProvider, route.modelId, route.providerName, "chat"),
|
|
@@ -311,9 +351,21 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
311
351
|
// key rotation so recovery cannot replace the ceiling along with the active credential.
|
|
312
352
|
const requestTransientPolicy = transientRetryPolicyFor(activeProvider);
|
|
313
353
|
let transientSendsUsed = 0;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
354
|
+
// A combo child also answers to the request's shared base allowance, at the cap its own ladder
|
|
355
|
+
// uses. Its first send is exempt: the combo reserved it before dispatching this target.
|
|
356
|
+
const sharedSendCap = requestTransientPolicy?.attempts ?? TRANSIENT_RETRY_MAX_ATTEMPTS;
|
|
357
|
+
let physicalSends = 0;
|
|
358
|
+
const remainingSharedSends = (): number => {
|
|
359
|
+
if (!sendBudget) return Number.POSITIVE_INFINITY;
|
|
360
|
+
const remaining = sendBudget.remainingBaseSends(sharedSendCap);
|
|
361
|
+
return physicalSends === 0 ? Math.max(1, remaining) : remaining;
|
|
362
|
+
};
|
|
363
|
+
const remainingTransientSends = (): number => Math.min(
|
|
364
|
+
requestTransientPolicy
|
|
365
|
+
? Math.max(0, requestTransientPolicy.attempts - transientSendsUsed)
|
|
366
|
+
: Number.POSITIVE_INFINITY,
|
|
367
|
+
remainingSharedSends(),
|
|
368
|
+
);
|
|
317
369
|
const transientSendAvailable = (): boolean => remainingTransientSends() > 0;
|
|
318
370
|
|
|
319
371
|
const send = async (request: AdapterRequest, recovery?: "rate-limit-429" | "key-429"): Promise<Response> => {
|
|
@@ -321,6 +373,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
321
373
|
// #2643: opted-in key-auth openai-chat providers retry pre-stream transient statuses on
|
|
322
374
|
// the native chat lane too; everyone else keeps reset-only semantics.
|
|
323
375
|
const remaining = remainingTransientSends();
|
|
376
|
+
if (sendBudget && remaining <= 0) throw new SendBudgetExhaustedError(safeHostLabel(request.url));
|
|
324
377
|
if (requestTransientPolicy && remaining <= 0) {
|
|
325
378
|
throw new Error("native Chat transient send budget exhausted before recovery dispatch");
|
|
326
379
|
}
|
|
@@ -343,7 +396,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
343
396
|
dispatchOverride: async (_input, init, execute) => {
|
|
344
397
|
if (!providerApiKeySelectionIsCurrent(config, route.providerName, activeProvider)) {
|
|
345
398
|
const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, activeProvider);
|
|
346
|
-
if (!current || !isNativeChatRouteEligible({ ...route, provider: current },
|
|
399
|
+
if (!current || !isNativeChatRouteEligible({ ...route, provider: current }, execution.chatBody, config)) {
|
|
347
400
|
throw new Error("Provider key selection is no longer available for native Chat");
|
|
348
401
|
}
|
|
349
402
|
activeProvider = current;
|
|
@@ -362,7 +415,15 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
362
415
|
const encoding = new Headers(init.headers).get("accept-encoding");
|
|
363
416
|
if (!headers.has("accept-encoding") && encoding) headers.set("accept-encoding", encoding);
|
|
364
417
|
if (init.signal?.aborted) throw init.signal.reason;
|
|
365
|
-
if (
|
|
418
|
+
if (sendBudget) {
|
|
419
|
+
// Backstop for sends the helper cannot see coming (a reset replay). The first
|
|
420
|
+
// report settles the combo's booking; each later one is charged and booked.
|
|
421
|
+
if (physicalSends > 0 && sendBudget.remainingBaseSends(sharedSendCap) <= 0) {
|
|
422
|
+
throw new SendBudgetExhaustedError(safeHostLabel(request.url));
|
|
423
|
+
}
|
|
424
|
+
physicalSends += 1;
|
|
425
|
+
sendBudget.used += 1;
|
|
426
|
+
} else if (!spendTracker?.charge()) throw new NativeChatSpendRefusal();
|
|
366
427
|
noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
|
|
367
428
|
// A reselected provider transport is still a physical send: the connection policy
|
|
368
429
|
// and manual-redirect ownership wrap the selected implementation (#4992).
|
|
@@ -433,7 +494,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
433
494
|
now: Date.now(),
|
|
434
495
|
attemptedKey: activeProvider.apiKey,
|
|
435
496
|
attemptedSelection: activeProvider._apiKeyAttempt,
|
|
436
|
-
promptCacheKey: typeof
|
|
497
|
+
promptCacheKey: typeof execution.chatBody.prompt_cache_key === "string" ? execution.chatBody.prompt_cache_key : undefined,
|
|
437
498
|
});
|
|
438
499
|
if (!rotated) break;
|
|
439
500
|
// Rotation also records the failed key's cooldown and persists the next healthy key.
|
|
@@ -455,6 +516,10 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
455
516
|
upstream.abort();
|
|
456
517
|
if (req.signal.aborted) return fail(499, "Client cancelled request", "client_cancelled");
|
|
457
518
|
const sendError = error instanceof UpstreamRetryEvidenceError ? error.cause : error;
|
|
519
|
+
if (sendBudget && sendError instanceof SendBudgetExhaustedError) {
|
|
520
|
+
// A decision this process made, answered as the Responses path answers it: 429, not 502.
|
|
521
|
+
return fail(429, sendError.message, SEND_BUDGET_EXHAUSTED_CODE, SEND_BUDGET_EXHAUSTED_CODE);
|
|
522
|
+
}
|
|
458
523
|
if (sendError instanceof NativeChatSpendRefusal) {
|
|
459
524
|
const refusal = workflowRefusalResponse("workflow-spend-exhausted", logCtx);
|
|
460
525
|
finishLog(429);
|
|
@@ -569,7 +634,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
569
634
|
translatorBudget,
|
|
570
635
|
signal: upstream.signal,
|
|
571
636
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
572
|
-
onFirstOutput
|
|
637
|
+
onFirstOutput,
|
|
573
638
|
onUsage: usage => {
|
|
574
639
|
if (!recordKeyWireAttemptUsage(logCtx, usage)) {
|
|
575
640
|
logCtx.usage = usage;
|
|
@@ -669,7 +734,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
669
734
|
attempt.usage = usage;
|
|
670
735
|
}
|
|
671
736
|
}
|
|
672
|
-
|
|
737
|
+
onFirstOutput?.();
|
|
673
738
|
try {
|
|
674
739
|
const serialized = requestedStream
|
|
675
740
|
? jsonCompletionSse(completion, requestedModel, translatorBudget)
|
|
@@ -18,7 +18,8 @@ import { sseFieldValue } from "../lib/sse-decoder";
|
|
|
18
18
|
import { enforceAnthropicImageLimits, sniffImageDimensions } from "../adapters/anthropic-image-guard";
|
|
19
19
|
import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
|
|
20
20
|
import { createToolCallIdAllocator } from "../adapters/tool-call-id";
|
|
21
|
-
import {
|
|
21
|
+
import { messagesToResponsesTranslation } from "../protocols/codecs/messages";
|
|
22
|
+
import { AnthropicRequestError, DesktopModelMappingUnavailableError, extractOcxEffortDirective, extractOcxRouteDirective, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
|
|
22
23
|
import { isKnownDesktop3pModelId, resolveDesktop3pAlias } from "../claude/desktop-3p";
|
|
23
24
|
import { resolveAlias, claudeCodeNativeAlias, legacyAliasForNative } from "../claude/alias";
|
|
24
25
|
import { recordDesktopRequest } from "../claude/desktop-health";
|
|
@@ -49,7 +50,8 @@ import { evidenceFromBody } from "../routing/request-evidence";
|
|
|
49
50
|
import { resolveWireProtocolOverride } from "./adapter-resolve";
|
|
50
51
|
import type { OcxConfig } from "../types";
|
|
51
52
|
import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress";
|
|
52
|
-
import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext
|
|
53
|
+
import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext } from "./request-log";
|
|
54
|
+
import { createFinalRequestLog } from "./inference/final-log";
|
|
53
55
|
import {
|
|
54
56
|
conversationIdFromClaudeMetadata,
|
|
55
57
|
getOrAllocateRequestSessionLane,
|
|
@@ -58,7 +60,21 @@ import {
|
|
|
58
60
|
sessionLaneIdFromRequest,
|
|
59
61
|
} from "./request-log-conversation";
|
|
60
62
|
import { responseWithDeferredRequestLog } from "./relay";
|
|
63
|
+
import { clientWireOf } from "./inference/client-wire";
|
|
64
|
+
import { directEncodersApply } from "./inference/client-encoder-delivery";
|
|
65
|
+
import type { ClientEncoderOption } from "./responses/core-options";
|
|
61
66
|
import { handleResponses } from "./responses";
|
|
67
|
+
import { upstreamWireForAdapter } from "../protocols/contract";
|
|
68
|
+
import { createProtocolEnvelope, type ProtocolEnvelope } from "../protocols/envelope";
|
|
69
|
+
import { featuresFromMessagesBody, type ProtocolFeature } from "../protocols/features";
|
|
70
|
+
import { credentialDomainFor, messagesBodyHasOpaqueState } from "../protocols/opaque-state";
|
|
71
|
+
import { hasAnthropicFailoverQuorum } from "../oauth/anthropic-routing";
|
|
72
|
+
import { checkRepresentable, unrepresentableMessage } from "../protocols/guard";
|
|
73
|
+
import { requestPathForLane } from "../protocols/path";
|
|
74
|
+
import { resolveApiSurfaceSettings, resolveProtocolSettings } from "../protocols/settings";
|
|
75
|
+
import { markProtocolBlocked, markProtocolEntry } from "../protocols/trace";
|
|
76
|
+
import { recordProtocolShadowPlan } from "../protocols/shadow-plan";
|
|
77
|
+
import { nativeMessagesDeclineReason, type NativeMessagesSelector } from "./messages-native-eligibility";
|
|
62
78
|
import {
|
|
63
79
|
isApiAuthRequired,
|
|
64
80
|
isDataPlaneAdmissionSecret,
|
|
@@ -148,11 +164,19 @@ export function buildClaudeReplayConfig(config: OcxConfig): OcxConfig {
|
|
|
148
164
|
};
|
|
149
165
|
}
|
|
150
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Messages exposure, shared by /v1/messages and /v1/messages/count_tokens so the two can never
|
|
169
|
+
* disagree. `resolveApiSurfaceSettings` is the only reader: an explicit
|
|
170
|
+
* `apiSurfaces.messages.enabled` wins, a malformed one closes the surface, and absence inherits
|
|
171
|
+
* `claudeCode.enabled`.
|
|
172
|
+
*/
|
|
151
173
|
function claudeInboundDisabled(config: OcxConfig): Response | null {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
174
|
+
const messages = resolveApiSurfaceSettings(config).messages;
|
|
175
|
+
if (messages.enabled) return null;
|
|
176
|
+
const detail = messages.source === "invalid"
|
|
177
|
+
? "config.apiSurfaces.messages is not a valid setting, so the surface stays closed"
|
|
178
|
+
: "GUI: API page Messages toggle / config.apiSurfaces.messages.enabled / config.claudeCode.enabled";
|
|
179
|
+
return anthropicErrorResponse(403, `Messages API is disabled (${detail})`, "permission_error");
|
|
156
180
|
}
|
|
157
181
|
|
|
158
182
|
async function readAnthropicBody(req: Request, budget: TranslatorBudget, maxBytes: number): Promise<unknown> {
|
|
@@ -234,7 +258,7 @@ function uuidFromHex(hex32: string): string {
|
|
|
234
258
|
return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;
|
|
235
259
|
}
|
|
236
260
|
|
|
237
|
-
function anthropicUsageToOcx(usage: Rec | undefined): { inputTokens: number; outputTokens: number; cachedInputTokens?: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number } | undefined {
|
|
261
|
+
export function anthropicUsageToOcx(usage: Rec | undefined): { inputTokens: number; outputTokens: number; cachedInputTokens?: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number } | undefined {
|
|
238
262
|
if (!usage) return undefined;
|
|
239
263
|
const num = (v: unknown) => typeof v === "number" ? v : 0;
|
|
240
264
|
const hasCache = usage.cache_read_input_tokens !== undefined || usage.cache_creation_input_tokens !== undefined;
|
|
@@ -428,7 +452,7 @@ export function tapAnthropicSseForLog(
|
|
|
428
452
|
* An empty id has no representable wire form, and forwarding `""` is what Anthropic
|
|
429
453
|
* rejects (#1767), so the request fails locally with a 400 before any upstream fetch.
|
|
430
454
|
*/
|
|
431
|
-
function sanitizePassthroughToolCallIds(messages: unknown[]): void {
|
|
455
|
+
export function sanitizePassthroughToolCallIds(messages: unknown[]): void {
|
|
432
456
|
const blocks: Rec[] = [];
|
|
433
457
|
for (const message of messages) {
|
|
434
458
|
if (!isRec(message) || !Array.isArray(message.content)) continue;
|
|
@@ -466,12 +490,7 @@ async function anthropicNativePassthrough(
|
|
|
466
490
|
logCtx.model = model;
|
|
467
491
|
logCtx.provider = "anthropic-native";
|
|
468
492
|
logCtx.requestedModel = model;
|
|
469
|
-
|
|
470
|
-
const finalize = (status: number, meta: { closeReason: PassthroughCloseReason | "non_stream" }) => {
|
|
471
|
-
if (!logIds || logged) return;
|
|
472
|
-
logged = true;
|
|
473
|
-
addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
|
|
474
|
-
};
|
|
493
|
+
const finalize = createFinalRequestLog(logIds, logCtx).finish;
|
|
475
494
|
|
|
476
495
|
const base = (config.claudeCode?.anthropicBaseUrl ?? "https://api.anthropic.com").replace(/\/$/, "");
|
|
477
496
|
const search = new URL(req.url).search;
|
|
@@ -718,6 +737,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
718
737
|
logCtx.surface = "claude";
|
|
719
738
|
const disabled = claudeInboundDisabled(config);
|
|
720
739
|
if (disabled) {
|
|
740
|
+
markProtocolBlocked(logCtx, { inbound: "messages", reasonCodes: ["surface-disabled"] });
|
|
721
741
|
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 403, { closeReason: "non_stream" });
|
|
722
742
|
return disabled;
|
|
723
743
|
}
|
|
@@ -731,6 +751,9 @@ async function handleClaudeMessagesWithBudget(
|
|
|
731
751
|
let effortRow: ParsedEffortRowId | null = null;
|
|
732
752
|
let fastRow: ParsedFastRowId | null = null;
|
|
733
753
|
let requestedModel = "";
|
|
754
|
+
// Built only under the reject policy; the legacy default leaves this request untouched.
|
|
755
|
+
let envelope: ProtocolEnvelope | undefined;
|
|
756
|
+
let messagesFeatures: () => Iterable<ProtocolFeature> = () => [];
|
|
734
757
|
try {
|
|
735
758
|
anthropicBody = await readAnthropicBody(req, translatorBudget, resolveInboundBodyLimitBytes(config.maxInboundBodyBytes));
|
|
736
759
|
// Defensive [1m] strip (devlog 138): clients normally remove the context-variant
|
|
@@ -797,7 +820,21 @@ async function handleClaudeMessagesWithBudget(
|
|
|
797
820
|
// caller's body with the caller's credential and never runs the Anthropic adapter, so the
|
|
798
821
|
// proxy-owned `speed` + beta (anthropic-speed wire) and its usage.speed observation would be
|
|
799
822
|
// silently skipped. Translation reaches the adapter, which owns both.
|
|
823
|
+
const messagesBody = anthropicBody;
|
|
824
|
+
if (isRec(messagesBody) && resolveProtocolSettings(config).unrepresentable === "reject") {
|
|
825
|
+
envelope = createProtocolEnvelope({ inbound: "messages", body: messagesBody, translatorBudget });
|
|
826
|
+
}
|
|
827
|
+
const sourceEnvelope = envelope;
|
|
828
|
+
// The bridge entry mark below reads these before an effort override rewrites `thinking`,
|
|
829
|
+
// which also fixes the envelope's cached features on the caller's own settings.
|
|
830
|
+
// Scanned once, like the envelope's cache, so a later mark reports the same features.
|
|
831
|
+
let scannedFeatures: ReadonlySet<ProtocolFeature> | undefined;
|
|
832
|
+
messagesFeatures = sourceEnvelope
|
|
833
|
+
? () => sourceEnvelope.features()
|
|
834
|
+
: () => (scannedFeatures ??= featuresFromMessagesBody(messagesBody));
|
|
800
835
|
if (!effortRow && !fastRow && isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model, cc)) {
|
|
836
|
+
markProtocolEntry(logCtx, { inbound: "messages", lane: "native", features: messagesFeatures });
|
|
837
|
+
recordProtocolShadowPlan(logCtx, config, { inbound: "messages", model: requestedModel });
|
|
801
838
|
return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
|
|
802
839
|
}
|
|
803
840
|
// Capture source semantics before effort rewriting or translation drops fields.
|
|
@@ -814,6 +851,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
814
851
|
anthropicBeta: req.headers.get("anthropic-beta") ?? undefined,
|
|
815
852
|
});
|
|
816
853
|
if (compatibility.decision === "reject") {
|
|
854
|
+
markProtocolBlocked(logCtx, { inbound: "messages", reasonCodes: ["compatibility-reject"], features: messagesFeatures });
|
|
817
855
|
logCtx.errorCode = "claude_compatibility_unsupported";
|
|
818
856
|
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 400, { closeReason: "non_stream" });
|
|
819
857
|
return anthropicErrorResponse(400, compatibility.reason!, "invalid_request_error");
|
|
@@ -826,6 +864,14 @@ async function handleClaudeMessagesWithBudget(
|
|
|
826
864
|
};
|
|
827
865
|
}
|
|
828
866
|
}
|
|
867
|
+
// Features are read here, before an effort override rewrites the thinking settings.
|
|
868
|
+
markProtocolEntry(logCtx, {
|
|
869
|
+
inbound: "messages",
|
|
870
|
+
lane: "bridge",
|
|
871
|
+
reasonCodes: effortRow ? ["effort-row"] : fastRow ? ["fast-row"] : [],
|
|
872
|
+
features: messagesFeatures,
|
|
873
|
+
});
|
|
874
|
+
recordProtocolShadowPlan(logCtx, config, { inbound: "messages", model: requestedModel });
|
|
829
875
|
if (isRec(anthropicBody) && effortOverride) {
|
|
830
876
|
anthropicBody.output_config = {
|
|
831
877
|
...(isRec(anthropicBody.output_config) ? anthropicBody.output_config : {}),
|
|
@@ -833,7 +879,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
833
879
|
};
|
|
834
880
|
delete anthropicBody.thinking;
|
|
835
881
|
}
|
|
836
|
-
const translation =
|
|
882
|
+
const translation = messagesToResponsesTranslation(anthropicBody, cc, translatorBudget);
|
|
837
883
|
internalBody = translation.body;
|
|
838
884
|
// The Anthropic translator builds its body from model/input/store/stream plus sampling
|
|
839
885
|
// fields only, so the caller intent is applied to the TRANSLATED body rather than the
|
|
@@ -877,9 +923,11 @@ async function handleClaudeMessagesWithBudget(
|
|
|
877
923
|
// the translated Anthropic SSE into a message JSON for non-streaming clients.
|
|
878
924
|
internalBody.stream = true;
|
|
879
925
|
|
|
926
|
+
let clientEncoder: ClientEncoderOption | undefined;
|
|
880
927
|
// Native ChatGPT passthrough (openai-responses forward) accepts only Codex-shaped
|
|
881
928
|
// bodies: it 400s on sampling params ("Unsupported parameter: max_output_tokens",
|
|
882
929
|
// verified live 2026-07-11). Strip them for that route; routed providers keep them.
|
|
930
|
+
let settledRoute: ReturnType<typeof routeModel> | undefined;
|
|
883
931
|
try {
|
|
884
932
|
const route = routeModel(config, internalBody.model as string, evidenceFromBody(internalBody));
|
|
885
933
|
// Same reason as the native Chat lane: this route can be sent from here, so
|
|
@@ -896,6 +944,10 @@ async function handleClaudeMessagesWithBudget(
|
|
|
896
944
|
);
|
|
897
945
|
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic", route.staticPolicy);
|
|
898
946
|
logCtx.routeDecision = route.routeDecision;
|
|
947
|
+
settledRoute = route;
|
|
948
|
+
if (directEncodersApply(config, route)) {
|
|
949
|
+
clientEncoder = { protocol: "messages", stream, model: requestedModel, inputTokenFloor: claudeRequestTokenFloor() };
|
|
950
|
+
}
|
|
899
951
|
if (route.provider.adapter === "openai-responses") {
|
|
900
952
|
delete internalBody.max_output_tokens;
|
|
901
953
|
delete internalBody.temperature;
|
|
@@ -939,6 +991,70 @@ async function handleClaudeMessagesWithBudget(
|
|
|
939
991
|
/* unknown model: let handleResponses shape the 404 */
|
|
940
992
|
}
|
|
941
993
|
|
|
994
|
+
// PF-08: a managed-key Anthropic route sends its Messages body natively. The caller-forward
|
|
995
|
+
// passthrough above was decided on the caller's own credential and never reaches this point.
|
|
996
|
+
const nativeSelector: NativeMessagesSelector = {
|
|
997
|
+
effortRow: !!effortRow, fastRow: !!fastRow, routeSelector: String(internalBody.model ?? ""), claudeCode: cc,
|
|
998
|
+
// PF-10: a stored second OAuth account turns on the bridge's rotation, so it stays there.
|
|
999
|
+
...(settledRoute?.provider.authMode === "oauth" ? { oauthFailoverQuorum: hasAnthropicFailoverQuorum() } : {}),
|
|
1000
|
+
};
|
|
1001
|
+
const nativeDecline = settledRoute && isRec(anthropicBody)
|
|
1002
|
+
? nativeMessagesDeclineReason(settledRoute, anthropicBody, config, nativeSelector)
|
|
1003
|
+
: "rollout-disabled";
|
|
1004
|
+
const nativeMessagesRoute = settledRoute && nativeDecline === undefined ? settledRoute : undefined;
|
|
1005
|
+
// With the switch on, a declined route says why on its bridge mark, as native Chat does.
|
|
1006
|
+
if (nativeDecline && nativeDecline !== "rollout-disabled") {
|
|
1007
|
+
markProtocolEntry(logCtx, {
|
|
1008
|
+
inbound: "messages",
|
|
1009
|
+
lane: "bridge",
|
|
1010
|
+
reasonCodes: [...(effortRow ? ["effort-row" as const] : fastRow ? ["fast-row" as const] : []), nativeDecline],
|
|
1011
|
+
features: messagesFeatures,
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
// Combo and policy children are judged per candidate (PF-07); an unknown model has no route.
|
|
1015
|
+
if (envelope && settledRoute && !settledRoute.combo && settledRoute.routeKind !== "policy") {
|
|
1016
|
+
const verdict = checkRepresentable({
|
|
1017
|
+
inbound: "messages",
|
|
1018
|
+
requestPath: requestPathForLane("messages", nativeMessagesRoute ? "native" : "bridge", upstreamWireForAdapter(settledRoute.provider.adapter)),
|
|
1019
|
+
features: envelope.features(),
|
|
1020
|
+
policy: "reject",
|
|
1021
|
+
});
|
|
1022
|
+
if (!verdict.ok) {
|
|
1023
|
+
markProtocolBlocked(logCtx, { inbound: "messages", reasonCodes: verdict.reasonCodes, features: verdict.features });
|
|
1024
|
+
logCtx.errorCode = "unsupported_feature";
|
|
1025
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 400, { closeReason: "non_stream" });
|
|
1026
|
+
return anthropicErrorResponse(400, unrepresentableMessage(verdict.features), "invalid_request_error");
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
// PF-10: opaque thinking state reaches only first-party Anthropic; under `reject` a native
|
|
1030
|
+
// route to anyone else refuses the request instead of sending it without that state.
|
|
1031
|
+
if (envelope && nativeMessagesRoute && !credentialDomainFor(nativeMessagesRoute.provider)?.firstPartyAnthropic
|
|
1032
|
+
&& messagesBodyHasOpaqueState(anthropicBody as Rec)) {
|
|
1033
|
+
markProtocolBlocked(logCtx, { inbound: "messages", reasonCodes: ["feature-unrepresentable", "opaque-state-stripped"], features: messagesFeatures });
|
|
1034
|
+
logCtx.errorCode = "unsupported_feature";
|
|
1035
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 400, { closeReason: "non_stream" });
|
|
1036
|
+
return anthropicErrorResponse(400, "The selected route cannot carry thinking signatures or redacted_thinking blocks", "invalid_request_error");
|
|
1037
|
+
}
|
|
1038
|
+
if (nativeMessagesRoute) {
|
|
1039
|
+
markProtocolEntry(logCtx, { inbound: "messages", lane: "native", features: messagesFeatures });
|
|
1040
|
+
let nativeBody: Rec;
|
|
1041
|
+
try {
|
|
1042
|
+
// Built from the source envelope when there is one, after the managed-client steps above.
|
|
1043
|
+
nativeBody = envelope ? envelope.freshBody() : anthropicBody as Rec;
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
if (!isTranslatorBudgetExceededError(err)) throw err;
|
|
1046
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 413, { closeReason: "non_stream" });
|
|
1047
|
+
return anthropicErrorResponse(413, "request translation buffer exceeded the safe limit", "request_too_large", "translation_buffer_limit");
|
|
1048
|
+
}
|
|
1049
|
+
const { handleNativeMessages } = await import("./messages-native");
|
|
1050
|
+
return await handleNativeMessages({
|
|
1051
|
+
req, config, logCtx, ...(logIds ? { logIds } : {}),
|
|
1052
|
+
route: nativeMessagesRoute, body: nativeBody, requestedModel, translatorBudget, selector: nativeSelector,
|
|
1053
|
+
// The one caller header the native lane is given; the builder allowlists it.
|
|
1054
|
+
callerAnthropicBeta: req.headers.get("anthropic-beta"),
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
|
|
942
1058
|
const headers = new Headers({ "content-type": "application/json" });
|
|
943
1059
|
let trustedClaudeMainAuth: { authorization: string; chatgptAccountId?: string } | undefined;
|
|
944
1060
|
for (const name of FORWARD_HEADERS) {
|
|
@@ -1008,12 +1124,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1008
1124
|
// via the terminal callbacks; routed streams get the Responses-vocabulary log tap
|
|
1009
1125
|
// BEFORE translation (the translated Anthropic stream has no response.completed
|
|
1010
1126
|
// frame, so tapping it records a bogus 502 with no usage/cache detail).
|
|
1011
|
-
|
|
1012
|
-
const finalizeNativeLog = (status: number, meta: { terminalStatus?: RequestLogEntry["terminalStatus"]; closeReason: "terminal" | "client_cancel" }) => {
|
|
1013
|
-
if (!logIds || nativeLogged) return;
|
|
1014
|
-
nativeLogged = true;
|
|
1015
|
-
addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
|
|
1016
|
-
};
|
|
1127
|
+
const finalizeNativeLog = createFinalRequestLog(logIds, logCtx).finish;
|
|
1017
1128
|
const upstream = await handleResponses(internalReq, buildClaudeReplayConfig(config), logCtx, {
|
|
1018
1129
|
// Routing keeps Claude-only sidecar overrides; admission policy must follow the live owner.
|
|
1019
1130
|
codexAuthPolicy: config,
|
|
@@ -1036,8 +1147,11 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1036
1147
|
...(logIds ? { onFirstOutput: () => recordFirstOutput(logCtx, logIds.start) } : {}),
|
|
1037
1148
|
onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForRequestLogTerminal(status, logCtx), { terminalStatus: status, closeReason: "terminal" }),
|
|
1038
1149
|
onNativePassthroughCancel: () => finalizeNativeLog(499, { closeReason: "client_cancel" }),
|
|
1150
|
+
...(clientEncoder ? { clientEncoder } : {}),
|
|
1039
1151
|
});
|
|
1040
1152
|
const response = logIds ? responseWithDeferredRequestLog(upstream, logIds.requestId, logIds.start, logCtx) : upstream;
|
|
1153
|
+
// Already in the Messages wire (direct encoder): no conversion.
|
|
1154
|
+
if (clientWireOf(upstream) === "messages") return response;
|
|
1041
1155
|
|
|
1042
1156
|
if (!response.ok) {
|
|
1043
1157
|
// Read the shared provenance verdict before consuming and re-wrapping the body. A refusal
|
|
@@ -1317,7 +1431,11 @@ export async function handleClaudeCountTokens(
|
|
|
1317
1431
|
if (wantsNativePassthrough(req, config, requestPolicy, model, cc)) {
|
|
1318
1432
|
return await anthropicNativePassthrough(req, config, { model, provider: "anthropic-native", surface: "claude" }, undefined, raw, "/v1/messages/count_tokens");
|
|
1319
1433
|
}
|
|
1320
|
-
|
|
1434
|
+
// PF-08: an eligible managed-key route counts the body the native lane would send.
|
|
1435
|
+
const nativeCountBody = resolveProtocolSettings(config).rollout.managedMessagesNative
|
|
1436
|
+
? (await import("./messages-native")).nativeMessagesCountBody(config, cc, raw, { fastRow: countFastRow !== null })
|
|
1437
|
+
: undefined;
|
|
1438
|
+
const inputTokens = estimateClaudeRequestTokens(nativeCountBody ?? raw, model);
|
|
1321
1439
|
return new Response(JSON.stringify({ input_tokens: inputTokens }), {
|
|
1322
1440
|
status: 200,
|
|
1323
1441
|
headers: { "Content-Type": "application/json" },
|