@bitkyc08/opencodex 2.55.0 → 2.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
2
|
+
import { redactSecretString } from "../../lib/redact";
|
|
3
|
+
import { isCyberPolicyMessage, isCyberPolicyCode } from "../../lib/errors";
|
|
4
|
+
import { isTranslatorBudgetExceededError } from "../../lib/translator-budget";
|
|
5
|
+
import { formatErrorResponse } from "../../bridge";
|
|
6
|
+
import {
|
|
7
|
+
UnsupportedContentEncodingError,
|
|
8
|
+
DecompressedBodyTooLargeError,
|
|
9
|
+
describeInboundBodyRefusal,
|
|
10
|
+
} from "../request-decompress";
|
|
11
|
+
import { comboCooldownRetryAfterSeconds } from "../../combos";
|
|
12
|
+
import type { AgentTaskRecoveryFailureReason } from "./agent-task-recovery";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Materialize an upstream error body only when the bounded reader observed a complete,
|
|
16
|
+
* display-safe payload. Partial timeout and over-limit prefixes are attacker-controlled,
|
|
17
|
+
* so callers keep their existing status-only fallback instead.
|
|
18
|
+
*/
|
|
19
|
+
export async function readDisplaySafeErrorText(
|
|
20
|
+
response: Response,
|
|
21
|
+
signal: AbortSignal,
|
|
22
|
+
fallback: string,
|
|
23
|
+
): Promise<string> {
|
|
24
|
+
try {
|
|
25
|
+
const body = await readBoundedResponseBody(response, { signal });
|
|
26
|
+
return body.displaySafe ? body.text : fallback;
|
|
27
|
+
} catch {
|
|
28
|
+
// Preserve the former Response.text().catch(fallback) contract. Request-abort
|
|
29
|
+
// classification remains owned by the surrounding response pipeline.
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
export interface NormalizedUpstreamErrorText {
|
|
36
|
+
safeText: string;
|
|
37
|
+
message?: string;
|
|
38
|
+
type?: string;
|
|
39
|
+
code?: string;
|
|
40
|
+
cyberPolicy: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract the structured provider error envelope without making `error.type` authoritative.
|
|
46
|
+
* Policy identity comes from the dedicated code (or the legacy message fallback); a credible
|
|
47
|
+
* upstream type is only carried through so callers do not erase provider diagnostics.
|
|
48
|
+
*/
|
|
49
|
+
export function normalizeUpstreamErrorText(text: string, fallback: string): NormalizedUpstreamErrorText {
|
|
50
|
+
const safeText = redactSecretString(text).slice(0, 500).trim() || fallback;
|
|
51
|
+
let message: string | undefined;
|
|
52
|
+
let type: string | undefined;
|
|
53
|
+
let code: string | undefined;
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(text) as Record<string, unknown>;
|
|
56
|
+
const response = parsed.response && typeof parsed.response === "object" && !Array.isArray(parsed.response)
|
|
57
|
+
? parsed.response as Record<string, unknown>
|
|
58
|
+
: undefined;
|
|
59
|
+
const candidates = [parsed.error, response?.error, response?.last_error, parsed.last_error, parsed];
|
|
60
|
+
const source = candidates.find((candidate): candidate is Record<string, unknown> => {
|
|
61
|
+
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) return false;
|
|
62
|
+
const record = candidate as Record<string, unknown>;
|
|
63
|
+
return [record.message, record.type, record.code].some(value => typeof value === "string");
|
|
64
|
+
});
|
|
65
|
+
if (!source) return { safeText, cyberPolicy: isCyberPolicyMessage(safeText) };
|
|
66
|
+
if (typeof source.message === "string" && source.message.trim()) {
|
|
67
|
+
message = redactSecretString(source.message.trim()).slice(0, 500);
|
|
68
|
+
}
|
|
69
|
+
if (typeof source.type === "string" && source.type.trim()) type = source.type.trim();
|
|
70
|
+
if (typeof source.code === "string" && source.code.trim()) code = source.code.trim();
|
|
71
|
+
} catch {
|
|
72
|
+
/* non-JSON upstream body — retain the bounded display-safe text */
|
|
73
|
+
}
|
|
74
|
+
const cyberPolicy = isCyberPolicyCode(code) || isCyberPolicyMessage(message ?? safeText);
|
|
75
|
+
return { safeText, message, type, code, cyberPolicy };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
export function decodeRequestErrorResponse(err: unknown, label: string): Response {
|
|
82
|
+
if (isTranslatorBudgetExceededError(err)) {
|
|
83
|
+
return formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
|
|
84
|
+
code: "translation_buffer_limit",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (err instanceof UnsupportedContentEncodingError) {
|
|
88
|
+
return formatErrorResponse(415, "invalid_request_error", err.message);
|
|
89
|
+
}
|
|
90
|
+
if (err instanceof DecompressedBodyTooLargeError) {
|
|
91
|
+
return formatErrorResponse(413, "inbound_body_too_large", describeInboundBodyRefusal(err));
|
|
92
|
+
}
|
|
93
|
+
console.warn(`[${label}] request body decode/parse failed: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`);
|
|
94
|
+
return formatErrorResponse(400, "invalid_request_error", "Invalid JSON body");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
export function comboUnavailableResponse(
|
|
101
|
+
message: string,
|
|
102
|
+
options?: { retryAfter?: string | null },
|
|
103
|
+
): Response {
|
|
104
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
105
|
+
const retryAfter = options?.retryAfter?.trim();
|
|
106
|
+
if (retryAfter && retryAfter.length > 0 && retryAfter.length <= 128) {
|
|
107
|
+
headers.set("Retry-After", retryAfter);
|
|
108
|
+
}
|
|
109
|
+
return new Response(
|
|
110
|
+
JSON.stringify({
|
|
111
|
+
error: { message, type: "server_error", code: "combo_unavailable" },
|
|
112
|
+
}),
|
|
113
|
+
{ status: 503, headers },
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
export function comboUnavailable(comboId: string, now = Date.now()): Response {
|
|
119
|
+
return comboUnavailableResponse(`No available targets for combo: ${comboId}`, {
|
|
120
|
+
retryAfter: comboCooldownRetryAfterSeconds(comboId, now),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Build the 499 JSON error the proxy returns when the client disconnects before the
|
|
129
|
+
* response completes (`client_cancelled`).
|
|
130
|
+
*/
|
|
131
|
+
export function clientCancelledResponse(): Response {
|
|
132
|
+
return formatErrorResponse(499, "client_cancelled", "Client cancelled request");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
export const UNREADABLE_ENCRYPTED_AGENT_TASK_MESSAGE =
|
|
137
|
+
"Routed V2 worker task is encrypted for the native ChatGPT backend and cannot be read by the selected provider. Use plaintext V2 agent-message delivery or select a native ChatGPT model.";
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
export function unreadableEncryptedAgentTaskResponse(reason?: AgentTaskRecoveryFailureReason): Response {
|
|
141
|
+
return new Response(
|
|
142
|
+
JSON.stringify({
|
|
143
|
+
error: {
|
|
144
|
+
message: UNREADABLE_ENCRYPTED_AGENT_TASK_MESSAGE,
|
|
145
|
+
type: "invalid_request_error",
|
|
146
|
+
code: "unreadable_encrypted_agent_task",
|
|
147
|
+
...(reason === undefined ? {} : { recovery_reason: reason }),
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
export const TARGET_INCOMPATIBLE_MESSAGE =
|
|
156
|
+
"No remaining combo target can continue this tool-bearing history because the reasoning required for replay is unavailable after the serving route changed. Start a new conversation or configure a combo target that can consume the available reasoning.";
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
export function targetIncompatibleResponse(): Response {
|
|
160
|
+
return new Response(
|
|
161
|
+
JSON.stringify({
|
|
162
|
+
error: {
|
|
163
|
+
message: TARGET_INCOMPATIBLE_MESSAGE,
|
|
164
|
+
type: "invalid_request_error",
|
|
165
|
+
code: "target_incompatible",
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
2
|
+
import {
|
|
3
|
+
isNativePassthroughSseResponse,
|
|
4
|
+
markNativePassthroughSseResponse,
|
|
5
|
+
isEagerRelaySseResponse,
|
|
6
|
+
markEagerRelaySseResponse,
|
|
7
|
+
} from "../relay";
|
|
8
|
+
|
|
9
|
+
// runTurn adapters own an event queue and perform their combo preflight before
|
|
10
|
+
// bridging. A second byte-stream reader would reinterpret that transport's
|
|
11
|
+
// already-committed event boundary and can replay custom adapter work.
|
|
12
|
+
export const runTurnAdapterSseResponses = new WeakSet<Response>();
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
// Whole-body policy for non-streaming upstream JSON responses (see the application/json
|
|
16
|
+
// branch of the passthrough return path). 32 MiB matches the continuation snapshot read
|
|
17
|
+
// bound and is far above any legitimate non-streaming completion, including base64 image
|
|
18
|
+
// payloads. The stall deadlines only govern the body transfer — generation time before
|
|
19
|
+
// the response headers is untouched. Generation after early/chunked headers but before
|
|
20
|
+
// the first body byte previously used the 30-second inactivity deadline; this call site
|
|
21
|
+
// gives it the full body deadline instead.
|
|
22
|
+
export const MAX_UPSTREAM_JSON_BODY_BYTES = 32 * 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
export const UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS = 180_000;
|
|
25
|
+
|
|
26
|
+
export const UPSTREAM_JSON_BODY_INACTIVITY_TIMEOUT_MS = 30_000;
|
|
27
|
+
|
|
28
|
+
export const UPSTREAM_JSON_BODY_READ_OPTIONS = {
|
|
29
|
+
maxBytes: MAX_UPSTREAM_JSON_BODY_BYTES,
|
|
30
|
+
totalTimeoutMs: UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS,
|
|
31
|
+
inactivityTimeoutMs: UPSTREAM_JSON_BODY_INACTIVITY_TIMEOUT_MS,
|
|
32
|
+
firstByteTimeoutMs: UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
export function finalizeOwnedTranslatorBudget(response: Response, budget: TranslatorBudget): Response {
|
|
39
|
+
if (!response.body) {
|
|
40
|
+
budget.dispose();
|
|
41
|
+
return response;
|
|
42
|
+
}
|
|
43
|
+
const reader = response.body.getReader();
|
|
44
|
+
let finalized = false;
|
|
45
|
+
const finalize = () => {
|
|
46
|
+
if (finalized) return;
|
|
47
|
+
finalized = true;
|
|
48
|
+
budget.dispose();
|
|
49
|
+
};
|
|
50
|
+
const body = new ReadableStream<Uint8Array>({
|
|
51
|
+
async pull(controller) {
|
|
52
|
+
try {
|
|
53
|
+
const result = await reader.read();
|
|
54
|
+
if (result.done) {
|
|
55
|
+
finalize();
|
|
56
|
+
controller.close();
|
|
57
|
+
} else {
|
|
58
|
+
controller.enqueue(result.value);
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
finalize();
|
|
62
|
+
controller.error(error);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
async cancel(reason) {
|
|
66
|
+
try { await reader.cancel(reason); } finally { finalize(); }
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const finalizedResponse = new Response(body, {
|
|
70
|
+
status: response.status,
|
|
71
|
+
statusText: response.statusText,
|
|
72
|
+
headers: response.headers,
|
|
73
|
+
});
|
|
74
|
+
if (isNativePassthroughSseResponse(response)) {
|
|
75
|
+
markNativePassthroughSseResponse(finalizedResponse);
|
|
76
|
+
}
|
|
77
|
+
if (isEagerRelaySseResponse(response)) {
|
|
78
|
+
markEagerRelaySseResponse(finalizedResponse);
|
|
79
|
+
}
|
|
80
|
+
return finalizedResponse;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
export function linkAbortSignal(upstream: AbortController, signal?: AbortSignal): () => void {
|
|
87
|
+
if (!signal) return () => {};
|
|
88
|
+
if (signal.aborted) {
|
|
89
|
+
upstream.abort(signal.reason);
|
|
90
|
+
return () => {};
|
|
91
|
+
}
|
|
92
|
+
const onAbort = () => upstream.abort(signal.reason);
|
|
93
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
94
|
+
return () => signal.removeEventListener("abort", onAbort);
|
|
95
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { sanitizeLogMetadataString } from "../../lib/redact";
|
|
2
|
+
import type { RouteResult } from "../../router";
|
|
3
|
+
import type { InboundWire } from "../../providers/registry";
|
|
4
|
+
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
5
|
+
import type { OcxConfig, OcxParsedRequest, OcxProviderConfig, TierDecision } from "../../types";
|
|
6
|
+
import {
|
|
7
|
+
resolveCodexModelEntitlements,
|
|
8
|
+
entitledCodexAccountIdsForModel,
|
|
9
|
+
} from "../../codex/model-entitlements";
|
|
10
|
+
import type { SubagentModelEligibleAccountIds } from "../../codex/subagent-model-fallback";
|
|
11
|
+
import { subagentFallbackNeedsModelEntitlements } from "../../codex/subagent-model-fallback";
|
|
12
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
13
|
+
import type { RequestLogContext } from "../request-log";
|
|
14
|
+
import type { HandleResponsesOptions } from "./core-options";
|
|
15
|
+
import { prepareEffortNormalization } from "../effort-policy";
|
|
16
|
+
import { providerModelResponsesUpstreamStreaming } from "../../providers/registry";
|
|
17
|
+
import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
|
|
18
|
+
import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
|
|
19
|
+
import { shouldPreparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
|
|
20
|
+
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
21
|
+
import { applyOpenAiVirtualModel } from "../../providers/openai-virtual-models";
|
|
22
|
+
import {
|
|
23
|
+
fastPolicyForModel,
|
|
24
|
+
serviceTierSupportFromPolicy,
|
|
25
|
+
SERVICE_TIER_ADAPTERS,
|
|
26
|
+
} from "../../providers/service-tier";
|
|
27
|
+
import {
|
|
28
|
+
tierObservationContext,
|
|
29
|
+
decideTier,
|
|
30
|
+
tierValueAfterDecision,
|
|
31
|
+
canonicalFastTierMarker,
|
|
32
|
+
} from "../../providers/fastwire";
|
|
33
|
+
import { multiAgentGuidanceText, injectDeveloperMessage, collabSurface } from "./collaboration";
|
|
34
|
+
import { multiAgentGuidanceEnabled } from "../../config";
|
|
35
|
+
import { isInjectionDebugEnabled } from "../../lib/debug-settings";
|
|
36
|
+
import { injectionDebugLog } from "../../lib/injection-debug-log";
|
|
37
|
+
import { recordAttemptRequestedEffort } from "../request-log";
|
|
38
|
+
import type { ResolvedFastPolicy } from "../../providers/fastwire";
|
|
39
|
+
|
|
40
|
+
export const MAX_FAST_WIRE_CAPABILITY_WARNINGS = 256;
|
|
41
|
+
|
|
42
|
+
export const warnedFastWireCapabilityGaps = new Set<string>();
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
export function warnFastWireCapabilityGap(providerName: string, modelId: string): void {
|
|
46
|
+
const safeProvider = sanitizeLogMetadataString(providerName) ?? "unknown";
|
|
47
|
+
const safeModel = sanitizeLogMetadataString(modelId) ?? "unknown";
|
|
48
|
+
const key = `${safeProvider}\0${safeModel}`;
|
|
49
|
+
if (warnedFastWireCapabilityGaps.has(key)) return;
|
|
50
|
+
if (warnedFastWireCapabilityGaps.size >= MAX_FAST_WIRE_CAPABILITY_WARNINGS) {
|
|
51
|
+
const oldest = warnedFastWireCapabilityGaps.values().next().value;
|
|
52
|
+
if (oldest !== undefined) warnedFastWireCapabilityGaps.delete(oldest);
|
|
53
|
+
}
|
|
54
|
+
warnedFastWireCapabilityGaps.add(key);
|
|
55
|
+
console.warn(
|
|
56
|
+
`[opencodex] Fast policy for ${safeProvider}/${safeModel} has service-tier capability but no Fast wire; preserving only caller-permitted tier behavior`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Keep this trust boundary deliberately narrow: only a key-auth Responses route may consume
|
|
63
|
+
* opaque child-task ciphertext, and the model's final wire override must still be Responses.
|
|
64
|
+
* Callers keep combo attempts on their existing native-only recovery/fail-closed behavior.
|
|
65
|
+
*/
|
|
66
|
+
export function canPassThroughEncryptedV2AgentTask(
|
|
67
|
+
route: RouteResult,
|
|
68
|
+
inboundWire: InboundWire,
|
|
69
|
+
): boolean {
|
|
70
|
+
if (route.combo !== undefined) return false;
|
|
71
|
+
const provider = route.provider;
|
|
72
|
+
if (
|
|
73
|
+
inboundWire !== "responses"
|
|
74
|
+
|| provider.allowEncryptedV2AgentTasks !== true
|
|
75
|
+
|| (provider.authMode ?? "key") !== "key"
|
|
76
|
+
) return false;
|
|
77
|
+
|
|
78
|
+
return resolveWireProtocolOverride(
|
|
79
|
+
route.providerName,
|
|
80
|
+
route.modelId,
|
|
81
|
+
provider,
|
|
82
|
+
inboundWire,
|
|
83
|
+
).adapter === "openai-responses";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
export async function resolveSubagentFallbackModelEligibility(args: {
|
|
88
|
+
config: OcxConfig;
|
|
89
|
+
fallbackChain: readonly string[] | null;
|
|
90
|
+
nativeMainReadsForbidden: boolean;
|
|
91
|
+
resolver: typeof resolveCodexModelEntitlements;
|
|
92
|
+
}): Promise<SubagentModelEligibleAccountIds | undefined> {
|
|
93
|
+
if (!subagentFallbackNeedsModelEntitlements(args.fallbackChain, args.config)) return undefined;
|
|
94
|
+
const excludeAccountIds = args.nativeMainReadsForbidden
|
|
95
|
+
? new Set([MAIN_CODEX_ACCOUNT_ID])
|
|
96
|
+
: undefined;
|
|
97
|
+
const snapshot = await args.resolver(args.config, { excludeAccountIds });
|
|
98
|
+
return (modelId) => {
|
|
99
|
+
const entitledAccountIds = entitledCodexAccountIdsForModel(snapshot, modelId);
|
|
100
|
+
return entitledAccountIds
|
|
101
|
+
? new Set([...entitledAccountIds].filter(accountId => !excludeAccountIds?.has(accountId)))
|
|
102
|
+
: undefined;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Apply every route-dependent request mutation against the final selected route.
|
|
109
|
+
* Must run only after subagent fallback has settled the model/provider.
|
|
110
|
+
*/
|
|
111
|
+
export async function applyFinalRouteRequestNormalization(args: {
|
|
112
|
+
parsed: OcxParsedRequest;
|
|
113
|
+
route: RouteResult;
|
|
114
|
+
config: OcxConfig;
|
|
115
|
+
req: Request;
|
|
116
|
+
logCtx: RequestLogContext;
|
|
117
|
+
inboundWire: InboundWire;
|
|
118
|
+
inboundTransport?: "websocket";
|
|
119
|
+
claudeGoAffinity?: HandleResponsesOptions["claudeGoAffinity"];
|
|
120
|
+
}): Promise<void> {
|
|
121
|
+
const { parsed, route, config, req, logCtx, inboundWire, inboundTransport } = args;
|
|
122
|
+
const effortSelector = prepareEffortNormalization(parsed, route);
|
|
123
|
+
|
|
124
|
+
// Only Anthropic message routes retain the Codex-facing selector. Other providers must keep
|
|
125
|
+
// their existing response.model contract even when their public and wire model ids differ.
|
|
126
|
+
const responseModelId = parsed.modelId;
|
|
127
|
+
const preserveAnthropicResponseModel = route.providerName === "anthropic"
|
|
128
|
+
|| route.provider.adapter === "anthropic";
|
|
129
|
+
|
|
130
|
+
// Apply the routed model id upstream: routing may strip a "<provider>/" namespace.
|
|
131
|
+
if (route.modelId !== parsed.modelId) {
|
|
132
|
+
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
133
|
+
(parsed._rawBody as { model?: string }).model = route.modelId;
|
|
134
|
+
}
|
|
135
|
+
parsed.modelId = route.modelId;
|
|
136
|
+
}
|
|
137
|
+
// Transport-neutral reliability policy (#875): applies to any Responses
|
|
138
|
+
// upstream whose final adapter is openai-responses, not only WS turns.
|
|
139
|
+
const responsesUpstreamStreaming = providerModelResponsesUpstreamStreaming(
|
|
140
|
+
route.providerName,
|
|
141
|
+
route.provider,
|
|
142
|
+
route.modelId,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
|
|
146
|
+
// this request will actually use (#404).
|
|
147
|
+
route.provider = resolveOpenCodeGoTransport(route.provider,
|
|
148
|
+
args.claudeGoAffinity ? args.claudeGoAffinity.sessionLane : getOrAllocateRequestSessionLane(req));
|
|
149
|
+
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
|
|
150
|
+
parsed._plaintextV2AgentMessages = shouldPreparePlaintextV2AgentMessages({
|
|
151
|
+
enabled: config.plaintextV2AgentMessages === true,
|
|
152
|
+
inboundWire,
|
|
153
|
+
canonicalChatGpt: isCanonicalOpenAiForwardProvider(route.provider),
|
|
154
|
+
requestBody: parsed._rawBody,
|
|
155
|
+
});
|
|
156
|
+
// Recompute from the original wire preference on every route, including fallback.
|
|
157
|
+
// A provider default never converts raw reasoning into a summary.
|
|
158
|
+
if (inboundWire === "responses" && parsed._rawBody) {
|
|
159
|
+
const summary = (parsed._rawBody as { reasoning?: { summary?: unknown } }).reasoning?.summary;
|
|
160
|
+
parsed.options.hideThinkingSummary = summary === "none"
|
|
161
|
+
|| (!summary && route.provider.showThinkingSummary !== true);
|
|
162
|
+
}
|
|
163
|
+
if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
|
|
164
|
+
logCtx.model = route.modelId;
|
|
165
|
+
logCtx.provider = route.providerName;
|
|
166
|
+
logCtx.providerAdapter = route.provider.adapter;
|
|
167
|
+
logCtx.routeDecision = route.routeDecision;
|
|
168
|
+
if (route.routeReason === "model-alias" || route.modelId !== responseModelId && responseModelId.includes("/")) logCtx.requestedAlias = responseModelId;
|
|
169
|
+
|
|
170
|
+
if (responsesUpstreamStreaming === false && route.provider.adapter === "openai-responses") {
|
|
171
|
+
parsed.stream = false;
|
|
172
|
+
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
173
|
+
(parsed._rawBody as Record<string, unknown>).stream = false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Generic Responses clients (e.g. AI-SDK apps) omit `store`, but the canonical
|
|
178
|
+
// forward Codex backend rejects a native request without an explicit store:false.
|
|
179
|
+
// Default it only there — every other Responses upstream (key-auth providers and
|
|
180
|
+
// custom forward gateways) intentionally keeps the omitted-store server-side
|
|
181
|
+
// default for previous_response_id reuse — and never override an explicit value.
|
|
182
|
+
if (
|
|
183
|
+
isCanonicalOpenAiForwardProvider(route.provider)
|
|
184
|
+
&& parsed._rawBody && typeof parsed._rawBody === "object"
|
|
185
|
+
&& (parsed._rawBody as Record<string, unknown>).store === undefined
|
|
186
|
+
) {
|
|
187
|
+
(parsed._rawBody as Record<string, unknown>).store = false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Final selected model before virtual wire-model rewriting (Pro aliases).
|
|
191
|
+
const finalSelectedModelId = route.modelId;
|
|
192
|
+
|
|
193
|
+
// Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
|
|
194
|
+
applyOpenAiVirtualModel(parsed, route, logCtx);
|
|
195
|
+
if (parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId) {
|
|
196
|
+
logCtx.resolvedModel = route.modelId;
|
|
197
|
+
logCtx.preserveResolvedModelFromRoute = true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Resolve Fast policy after the final route/wire settles. A1 records the decision on parsed
|
|
201
|
+
// options; the Responses adapter owns the final outbound body write.
|
|
202
|
+
const fastPolicy = fastPolicyForModel(
|
|
203
|
+
route.provider,
|
|
204
|
+
route.modelId,
|
|
205
|
+
route.providerName,
|
|
206
|
+
inboundWire,
|
|
207
|
+
config.providers[route.providerName],
|
|
208
|
+
);
|
|
209
|
+
const modelServiceTierSupport = serviceTierSupportFromPolicy(fastPolicy);
|
|
210
|
+
const callerTier = parsed.options.serviceTier;
|
|
211
|
+
// The ChatGPT-internal Codex backend echoes `service_tier: "default"` even on turns it
|
|
212
|
+
// scheduled as priority, so its echo cannot confirm OR deny Fast. Believing it reported every
|
|
213
|
+
// Fast request as `response-declined` (#2558). The public API's echo stays authoritative.
|
|
214
|
+
parsed.options.tierObservation = tierObservationContext(
|
|
215
|
+
fastPolicy,
|
|
216
|
+
config.fastMode,
|
|
217
|
+
callerTier,
|
|
218
|
+
isCanonicalOpenAiForwardProvider(route.provider) ? false : undefined,
|
|
219
|
+
);
|
|
220
|
+
parsed.options.tierDecision = decideTier(fastPolicy, config.fastMode, callerTier);
|
|
221
|
+
parsed.options.serviceTier = tierValueAfterDecision(parsed.options.tierDecision, callerTier);
|
|
222
|
+
if (fastPolicy.capability === true && fastPolicy.fastWire === null) {
|
|
223
|
+
warnFastWireCapabilityGap(route.providerName, route.modelId);
|
|
224
|
+
}
|
|
225
|
+
applyServiceTierGate(
|
|
226
|
+
route.provider,
|
|
227
|
+
parsed._rawBody,
|
|
228
|
+
parsed.options,
|
|
229
|
+
route.modelId,
|
|
230
|
+
route.providerName,
|
|
231
|
+
inboundWire,
|
|
232
|
+
fastPolicy,
|
|
233
|
+
);
|
|
234
|
+
if (modelServiceTierSupport === false) {
|
|
235
|
+
logCtx.requestedServiceTier = undefined;
|
|
236
|
+
logCtx.requestedSpeedLabel = undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
{
|
|
240
|
+
const guidance = await multiAgentGuidanceText(parsed, {
|
|
241
|
+
multiAgentGuidanceEnabled: config.multiAgentGuidanceEnabled,
|
|
242
|
+
codexAccountNamespace: route.codexAccountNamespace,
|
|
243
|
+
injectionModel: config.injectionModel,
|
|
244
|
+
injectionEffort: config.injectionEffort,
|
|
245
|
+
subagentModels: config.subagentModels,
|
|
246
|
+
subagentModelFallback: config.subagentModelFallback,
|
|
247
|
+
injectionPrompt: config.injectionPrompt,
|
|
248
|
+
});
|
|
249
|
+
if (guidance) {
|
|
250
|
+
injectDeveloperMessage(parsed, guidance);
|
|
251
|
+
if (isInjectionDebugEnabled()) {
|
|
252
|
+
injectionDebugLog(`[opencodex] ${route.modelId}: multi-agent guidance injected (surface=${collabSurface(parsed)}, guidanceEnabled=${multiAgentGuidanceEnabled(config)}, ${guidance.length} chars)`);
|
|
253
|
+
}
|
|
254
|
+
} else if (isInjectionDebugEnabled() && collabSurface(parsed) !== null) {
|
|
255
|
+
injectionDebugLog(`[opencodex] ${route.modelId}: collab surface=${collabSurface(parsed)}, guidance silent (effort=${parsed.options.reasoning ?? "unset"}, injectionModel=${config.injectionModel ?? "unset"})`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
{
|
|
260
|
+
const { applyPinnedEffort } = await import("../effort-policy");
|
|
261
|
+
const pinned = applyPinnedEffort(parsed, route, config, effortSelector);
|
|
262
|
+
if (pinned) {
|
|
263
|
+
logCtx.requestedEffort = pinned.from ? `${pinned.from}->${pinned.to}` : pinned.to;
|
|
264
|
+
if (isInjectionDebugEnabled()) {
|
|
265
|
+
injectionDebugLog(`[opencodex] ${route.modelId}: pinned reasoning effort applied (${pinned.from ?? "none"} -> ${pinned.to})`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
{
|
|
271
|
+
const { applyEffortCap, effortCapAppliesTo, supportedLadderFor } = await import("../effort-policy");
|
|
272
|
+
const surface = collabSurface(parsed);
|
|
273
|
+
if (effortCapAppliesTo(surface, req.headers, config, parsed._compactionRequest === true)) {
|
|
274
|
+
const capped = applyEffortCap(parsed, req.headers, config, supportedLadderFor(route));
|
|
275
|
+
if (capped) {
|
|
276
|
+
logCtx.requestedEffort = `${capped.from}->${capped.to}`;
|
|
277
|
+
if (isInjectionDebugEnabled()) {
|
|
278
|
+
injectionDebugLog(`[opencodex] ${route.modelId}: effort cap applied (${capped.from} -> ${capped.to}, ${capped.subagent ? "sub-agent" : "main"} turn)`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} else if (isInjectionDebugEnabled() && (config.effortCap || config.subagentEffortCap)) {
|
|
282
|
+
injectionDebugLog(`[opencodex] ${route.modelId}: effort cap skipped (surface=${surface ?? "none"}, v2 feature only)`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
{
|
|
287
|
+
const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../../codex/catalog");
|
|
288
|
+
const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, finalSelectedModelId)
|
|
289
|
+
? nativeEffortClamp(route.modelId, parsed.options.reasoning)
|
|
290
|
+
: null;
|
|
291
|
+
if (clamped) {
|
|
292
|
+
parsed.options.reasoning = clamped;
|
|
293
|
+
const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
|
|
294
|
+
if (raw?.reasoning && typeof raw.reasoning === "object") raw.reasoning.effort = clamped;
|
|
295
|
+
logCtx.requestedEffort = `${logCtx.requestedEffort ?? "max"}->${clamped}`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
recordAttemptRequestedEffort(logCtx);
|
|
299
|
+
logCtx.modelSupportsServiceTier = SERVICE_TIER_ADAPTERS.has(route.provider.adapter)
|
|
300
|
+
? modelServiceTierSupport
|
|
301
|
+
: undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Service-tier capability gate, applied after the final route/wire is settled. A
|
|
307
|
+
* provider explicitly documented as NOT supporting `service_tier` must never
|
|
308
|
+
* receive it: strip the field and clear the logging value even when the caller
|
|
309
|
+
* supplied one (fail closed). A policy-produced canonical Fast decision has
|
|
310
|
+
* already passed capability validation and cannot be vetoed by Chat's caller
|
|
311
|
+
* forwarding permission. On unclassified routes every caller tier remains subject
|
|
312
|
+
* to `forwardCallerTier`.
|
|
313
|
+
*/
|
|
314
|
+
export function applyServiceTierGate(
|
|
315
|
+
provider: OcxProviderConfig,
|
|
316
|
+
rawBody: unknown,
|
|
317
|
+
options: { serviceTier?: string; tierDecision?: TierDecision },
|
|
318
|
+
modelId?: string,
|
|
319
|
+
providerName?: string,
|
|
320
|
+
inbound: InboundWire = "responses",
|
|
321
|
+
resolvedPolicy?: ResolvedFastPolicy,
|
|
322
|
+
): void {
|
|
323
|
+
// A direct unit caller without a model id retains the historical tri-state behavior for
|
|
324
|
+
// adapters outside the OpenAI service-tier family. Once a model is known, resolve the final
|
|
325
|
+
// model adapter as well: an explicit override to Anthropic (or another non-OpenAI wire) must
|
|
326
|
+
// not carry a caller-supplied `service_tier` through a route that cannot forward it.
|
|
327
|
+
if (modelId === undefined && !SERVICE_TIER_ADAPTERS.has(provider.adapter)) return;
|
|
328
|
+
const policy = modelId === undefined
|
|
329
|
+
? undefined
|
|
330
|
+
: resolvedPolicy ?? fastPolicyForModel(provider, modelId, providerName, inbound);
|
|
331
|
+
const forwardCallerTier = modelId === undefined
|
|
332
|
+
? provider.supportsServiceTier !== false
|
|
333
|
+
: policy!.forwardCallerTier;
|
|
334
|
+
const rawTier = rawBody && typeof rawBody === "object"
|
|
335
|
+
? (rawBody as Record<string, unknown>).service_tier
|
|
336
|
+
: undefined;
|
|
337
|
+
const canonicalDecision = options.tierDecision?.kind === "set";
|
|
338
|
+
const callerTierIsForeign = rawTier !== undefined
|
|
339
|
+
&& (typeof rawTier !== "string" || canonicalFastTierMarker(rawTier) === undefined);
|
|
340
|
+
const dropForeignCallerTier = policy?.capability === true
|
|
341
|
+
&& policy.fastWire?.kind === "service-tier"
|
|
342
|
+
&& policy.fastWire?.foreignCallerTiers === "drop"
|
|
343
|
+
&& callerTierIsForeign;
|
|
344
|
+
if (policy && policy.capability !== false && canonicalDecision) return;
|
|
345
|
+
if (forwardCallerTier && !dropForeignCallerTier) return;
|
|
346
|
+
if (rawBody && typeof rawBody === "object") {
|
|
347
|
+
delete (rawBody as Record<string, unknown>).service_tier;
|
|
348
|
+
}
|
|
349
|
+
options.serviceTier = undefined;
|
|
350
|
+
}
|