@bitkyc08/opencodex 2.55.0-preview.20260914 → 2.56.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.
Files changed (167) hide show
  1. package/gui/dist/assets/{index-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -0,0 +1,152 @@
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
+ }
@@ -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
+ }