@bitkyc08/opencodex 2.41.0 → 2.43.0-preview.20260906
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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-VGeQEZ_v.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +54 -24
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/identity.ts +8 -2
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +259 -29
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +47 -13
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +49 -9
- package/src/cli/account-extended.ts +7 -1
- package/src/cli/capabilities.ts +15 -4
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/observe.ts +3 -1
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +272 -32
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +44 -5
- package/src/codex/catalog/metadata.ts +149 -14
- package/src/codex/catalog/native-models.ts +116 -4
- package/src/codex/catalog/parsing.ts +122 -8
- package/src/codex/catalog/provider-fetch.ts +154 -23
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +169 -0
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +99 -34
- package/src/codex/injected-marker.ts +30 -4
- package/src/codex/journal.ts +14 -0
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +474 -178
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/account-quota-rank.ts +40 -1
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/chatgpt-device.ts +187 -0
- package/src/oauth/chatgpt.ts +31 -4
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +140 -29
- package/src/oauth/log.ts +3 -0
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/muse-subscription-usage.ts +95 -0
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +625 -98
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +122 -28
- package/src/server/live.ts +18 -4
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +45 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +510 -57
- package/src/server/responses/empty-completion-guard.ts +4 -0
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/request.ts +8 -0
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-B2YjLA-i.css +0 -1
- package/gui/dist/assets/index-aPup8CKb.js +0 -112
package/src/adapters/kiro.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { resolveKiroApiRegion, resolveKiroRequestProfile } from "../oauth/kiro";
|
|
|
5
5
|
import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../providers/kiro-models";
|
|
6
6
|
import { modelRecordValue } from "../reasoning-effort";
|
|
7
7
|
import { parseKiroEvent } from "./kiro-events";
|
|
8
|
+
import { calibrateKiroEstimate, recordKiroCalibration, rekeyKiroCalibration } from "./kiro-calibration";
|
|
8
9
|
import {
|
|
9
10
|
classifyKiroEventError,
|
|
10
11
|
classifyKiroHttpError,
|
|
@@ -186,6 +187,85 @@ function estimateKiroTokens(text: string, modelId?: string): number {
|
|
|
186
187
|
return estimateTokens(text, modelId ? `kiro/${modelId}` : "kiro");
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
/** Hangul/Han/kana ranges, matching the shared estimator's own CJK classification. */
|
|
191
|
+
function kiroCjkCount(text: string): number {
|
|
192
|
+
let cjk = 0;
|
|
193
|
+
for (let i = 0; i < text.length; i++) {
|
|
194
|
+
const c = text.charCodeAt(i);
|
|
195
|
+
if (
|
|
196
|
+
(c >= 0xac00 && c <= 0xd7a3) || (c >= 0x1100 && c <= 0x11ff) || (c >= 0x3130 && c <= 0x318f)
|
|
197
|
+
|| (c >= 0x4e00 && c <= 0x9fff) || (c >= 0x3400 && c <= 0x4dbf) || (c >= 0x3040 && c <= 0x30ff)
|
|
198
|
+
) cjk++;
|
|
199
|
+
}
|
|
200
|
+
return cjk;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Token estimate for walked payload text, with the wire expansion applied to the Latin portion
|
|
205
|
+
* only. Splitting here rather than inside the shared estimator keeps that module pure and
|
|
206
|
+
* provider-neutral: the expansion is a fact about Kiro's wire, not about tokenization.
|
|
207
|
+
*/
|
|
208
|
+
function estimateKiroWireTokens(text: string, modelId: string): number {
|
|
209
|
+
if (!text) return 0;
|
|
210
|
+
const cjk = kiroCjkCount(text);
|
|
211
|
+
if (cjk === 0) return Math.ceil(estimateKiroTokens(text, modelId) * KIRO_LATIN_WIRE_EXPANSION);
|
|
212
|
+
const latinTokens = estimateKiroTokens("x".repeat(text.length - cjk), modelId);
|
|
213
|
+
const cjkTokens = estimateKiroTokens("\uac00".repeat(cjk), modelId);
|
|
214
|
+
return Math.ceil(latinTokens * KIRO_LATIN_WIRE_EXPANSION + cjkTokens);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Structural cost of one conversation entry, in tokens.
|
|
219
|
+
*
|
|
220
|
+
* The walker below concatenates message TEXT, but the wire carries JSON: per-entry keys
|
|
221
|
+
* (`userInputMessage`, `content`, `modelId`, `origin`) and role framing. That is charged
|
|
222
|
+
* upstream and is invisible to a text-only count, so without it a long conversation drifts
|
|
223
|
+
* further below the real charge with every turn added — an error proportional to entry COUNT,
|
|
224
|
+
* which no per-character ratio can recover.
|
|
225
|
+
*
|
|
226
|
+
* Regressing serialized bodies against what the walker counts, over eleven payload sizes from
|
|
227
|
+
* 3 to 701 entries:
|
|
228
|
+
*
|
|
229
|
+
* bodyBytes = 1.0422 * walkedChars + 66.7 * entries + 68
|
|
230
|
+
*
|
|
231
|
+
* 66.7 bytes at the measured 2.433 bytes per charged token is 27.4 tokens per entry. The
|
|
232
|
+
* earlier value of 12 was a conservative hand-fit taken before that regression existed, and
|
|
233
|
+
* being less than half the real cost is precisely why the estimate decayed with conversation
|
|
234
|
+
* length: an under-charge of ~15 tokens per entry is invisible across four messages and
|
|
235
|
+
* dominant across seven hundred.
|
|
236
|
+
*
|
|
237
|
+
* Cross-checked against 4,090 recorded requests, where real traffic averages 1,310 bytes per
|
|
238
|
+
* message: 66.7 bytes is 5% of that, so this term charges framing and is not quietly absorbing
|
|
239
|
+
* message content.
|
|
240
|
+
*/
|
|
241
|
+
const KIRO_ENTRY_FRAMING_TOKENS = 27;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Multiplier reconciling the LATIN text estimate with what the wire charges for that same text.
|
|
245
|
+
*
|
|
246
|
+
* The shared estimator counts Latin text at 2.8 chars/token, while the wire charges 2.433 bytes
|
|
247
|
+
* per token at 1.0422 bytes per walked character — an effective 2.334 chars/token, and
|
|
248
|
+
* 2.8 / 2.334 = 1.199.
|
|
249
|
+
*
|
|
250
|
+
* The evidence that the split between this term and `KIRO_ENTRY_FRAMING_TOKENS` is right is its
|
|
251
|
+
* stability: holding framing at 27, the multiplier the charge implies stays within 1.189-1.209
|
|
252
|
+
* across a 230x range of conversation sizes. A mis-specified split drifts with size, and the
|
|
253
|
+
* earlier 1.12/12 pair did — its accuracy fell from 0.92 at four messages to 0.87 at seven
|
|
254
|
+
* hundred.
|
|
255
|
+
*
|
|
256
|
+
* LATIN ONLY, deliberately. 2.433 bytes/token is a property of this traffic mix, which is Latin
|
|
257
|
+
* and code. A Hangul character is three UTF-8 bytes but roughly one token, so its bytes-per-token
|
|
258
|
+
* is entirely different and a Latin-derived byte rate says nothing about it. Scaling CJK by this
|
|
259
|
+
* factor bills Hangul at 1.25 chars/token, against recorded ground truth that already places the
|
|
260
|
+
* shared 1.5 ratio at 0.90 of the authoritative count — an over-charge that would compact Korean
|
|
261
|
+
* threads early.
|
|
262
|
+
*
|
|
263
|
+
* This is NOT JSON escaping, despite what an earlier version of this comment claimed. Measured
|
|
264
|
+
* directly, `JSON.stringify` expands prose by 1.012 (Latin) to 1.019 (Korean), nowhere near 1.2.
|
|
265
|
+
* Escaping is real but small, and is already inside the byte measurement this factor comes from.
|
|
266
|
+
*/
|
|
267
|
+
const KIRO_LATIN_WIRE_EXPANSION = 1.2;
|
|
268
|
+
|
|
189
269
|
function estimateKiroPayloadInputTokens(payload: Record<string, unknown>, modelId: string): number {
|
|
190
270
|
const conversationState = (payload as {
|
|
191
271
|
conversationState?: {
|
|
@@ -216,7 +296,9 @@ function estimateKiroPayloadInputTokens(payload: Record<string, unknown>, modelI
|
|
|
216
296
|
if (assistant.toolUses?.length) parts.push(serializeForUsage(assistant.toolUses));
|
|
217
297
|
}
|
|
218
298
|
}
|
|
219
|
-
return
|
|
299
|
+
return estimateKiroWireTokens(parts.join("\n"), modelId)
|
|
300
|
+
+ imageTokens
|
|
301
|
+
+ entries.length * KIRO_ENTRY_FRAMING_TOKENS;
|
|
220
302
|
}
|
|
221
303
|
|
|
222
304
|
function shouldCountStablePromptOverhead(parsed: OcxParsedRequest): boolean {
|
|
@@ -990,6 +1072,11 @@ async function* parseKiroAttempt(
|
|
|
990
1072
|
// the attempt boundary. Anything the inner parser leaves behind is flushed before the terminal.
|
|
991
1073
|
const deferred: AdapterEvent[] = [];
|
|
992
1074
|
const retention = createKiroAttemptRetention(budget);
|
|
1075
|
+
// Shared box: the inner parser stages its calibration observation here on the completion path,
|
|
1076
|
+
// and this wrapper decides whether the attempt was terminal enough to commit it. A box rather
|
|
1077
|
+
// than a return field because the completion path has a dozen terminal returns and threading a
|
|
1078
|
+
// field through every one of them is exactly the kind of edit that misses one.
|
|
1079
|
+
const attemptCalibration: { value?: { conversationId: string; estimated: number; charged: number } } = {};
|
|
993
1080
|
const attempt = parseKiroAttemptEvents(
|
|
994
1081
|
response,
|
|
995
1082
|
budget,
|
|
@@ -1001,12 +1088,22 @@ async function* parseKiroAttempt(
|
|
|
1001
1088
|
conversationId,
|
|
1002
1089
|
deferred,
|
|
1003
1090
|
retention,
|
|
1091
|
+
attemptCalibration,
|
|
1004
1092
|
contextInputEstimate,
|
|
1005
1093
|
priorEmittedOutput,
|
|
1006
1094
|
);
|
|
1007
1095
|
let handedOff = false;
|
|
1008
1096
|
try {
|
|
1009
1097
|
const result = yield* attempt;
|
|
1098
|
+
// A staged observation only counts when this attempt is the LAST one for the user turn. An
|
|
1099
|
+
// attempt that asks for the bounded fallback streams again against a rebuilt payload, so
|
|
1100
|
+
// committing here would move the factor twice for one turn and score the second observation
|
|
1101
|
+
// against a payload the first had already inflated.
|
|
1102
|
+
const staged = attemptCalibration.value;
|
|
1103
|
+
attemptCalibration.value = undefined;
|
|
1104
|
+
if (staged && !result.needsFallback) {
|
|
1105
|
+
recordKiroCalibration(staged.conversationId, staged.estimated, staged.charged);
|
|
1106
|
+
}
|
|
1010
1107
|
for (const event of deferred.splice(0)) {
|
|
1011
1108
|
try { yield event; } finally { retention.releaseEvent(event); }
|
|
1012
1109
|
}
|
|
@@ -1028,10 +1125,13 @@ async function* parseKiroAttemptEvents(
|
|
|
1028
1125
|
conversationId: string | undefined,
|
|
1029
1126
|
deferred: AdapterEvent[],
|
|
1030
1127
|
retention: KiroAttemptRetention,
|
|
1128
|
+
attemptCalibration: { value?: { conversationId: string; estimated: number; charged: number } },
|
|
1031
1129
|
contextInputEstimate?: number,
|
|
1032
1130
|
priorEmittedOutput = false,
|
|
1033
1131
|
): AsyncGenerator<AdapterEvent, KiroAttemptParseResult> {
|
|
1034
1132
|
const emptyResult = (): KiroAttemptParseResult => ({ assistantText: "", sawReasoning: false });
|
|
1133
|
+
// Every early return below is a failure path that stages nothing; only the completion path
|
|
1134
|
+
// writes `attemptCalibration`, and the wrapper decides whether to commit it.
|
|
1035
1135
|
if (!response.body) {
|
|
1036
1136
|
return {
|
|
1037
1137
|
...emptyResult(),
|
|
@@ -1346,7 +1446,13 @@ async function* parseKiroAttemptEvents(
|
|
|
1346
1446
|
if (ev.stopReason !== undefined) stopReason = ev.stopReason;
|
|
1347
1447
|
break;
|
|
1348
1448
|
case "message_metadata":
|
|
1349
|
-
if (isValidKiroConversationId(ev.conversationId))
|
|
1449
|
+
if (isValidKiroConversationId(ev.conversationId)) {
|
|
1450
|
+
// Kiro can answer under a different conversation id than the request was built with.
|
|
1451
|
+
// Carry the calibration entry across so the record below finds its own raw estimate
|
|
1452
|
+
// instead of silently falling back to the already-corrected value.
|
|
1453
|
+
rekeyKiroCalibration(returnedConversationId, ev.conversationId);
|
|
1454
|
+
returnedConversationId = ev.conversationId;
|
|
1455
|
+
}
|
|
1350
1456
|
break;
|
|
1351
1457
|
case "content":
|
|
1352
1458
|
if (ev.modelId) {
|
|
@@ -1475,6 +1581,29 @@ async function* parseKiroAttemptEvents(
|
|
|
1475
1581
|
...(contextWindowState.value ? { upstreamContextWindow: contextWindowState.value } : {}),
|
|
1476
1582
|
});
|
|
1477
1583
|
}
|
|
1584
|
+
// Upstream just told us what this payload cost. The ratio between that and our pre-request
|
|
1585
|
+
// estimate is this conversation's own measured error, and it is the only feedback the
|
|
1586
|
+
// estimator ever receives.
|
|
1587
|
+
//
|
|
1588
|
+
// Staged, not recorded. An attempt that sets `needsFallback` is not over: the adapter rebuilds
|
|
1589
|
+
// the payload and streams a second time for the SAME user turn. Learning here would apply the
|
|
1590
|
+
// fresh factor to that rebuild and then learn again from it, so one turn would move the factor
|
|
1591
|
+
// twice and the second observation would score a payload the first had already inflated. Only
|
|
1592
|
+
// the outer parser knows whether an attempt is terminal, so it commits.
|
|
1593
|
+
//
|
|
1594
|
+
// Subtract the output first. `contextUsageTotalFloor` is the absolute context size AFTER the
|
|
1595
|
+
// response (`OcxUsage.contextTotalTokens`, types/request.ts), while `contextInputEstimate`
|
|
1596
|
+
// covers the request payload alone. Dividing one by the other would charge generated tokens to
|
|
1597
|
+
// prompt-tokenization error, so a short prompt answered at length would learn a large factor
|
|
1598
|
+
// and inflate every later request in that conversation — the premature compaction this work
|
|
1599
|
+
// exists to prevent.
|
|
1600
|
+
const chargedTotal = contextUsageTotalFloor();
|
|
1601
|
+
if (chargedTotal !== undefined && contextInputEstimate !== undefined) {
|
|
1602
|
+
const chargedInput = chargedTotal - finalUsage.outputTokens;
|
|
1603
|
+
if (chargedInput > 0 && returnedConversationId) {
|
|
1604
|
+
attemptCalibration.value = { conversationId: returnedConversationId, estimated: contextInputEstimate, charged: chargedInput };
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1478
1607
|
// Native stop metadata proves that this inference ended, but it does not prove that ordinary
|
|
1479
1608
|
// text is a final answer. Kiro has emitted END_TURN for progress prose, so tool-enabled turns
|
|
1480
1609
|
// still require the private completion call to distinguish commentary from completion (#531).
|
|
@@ -1934,7 +2063,10 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
1934
2063
|
if (profileArn) headers["x-amzn-kiro-profile-arn"] = profileArn;
|
|
1935
2064
|
const built = buildKiroPayload(parsed, profileArn, forcedCompletionMode, wireClient);
|
|
1936
2065
|
await normalizeKiroImages(built.payload);
|
|
1937
|
-
|
|
2066
|
+
// Apply what earlier turns of THIS conversation measured. An unseen conversation is
|
|
2067
|
+
// unchanged, so a first turn behaves exactly as it would without calibration.
|
|
2068
|
+
const rawContextInputEstimate = estimateKiroPayloadInputTokens(built.payload, parsed.modelId);
|
|
2069
|
+
const contextInputEstimate = calibrateKiroEstimate(built.conversationId, rawContextInputEstimate);
|
|
1938
2070
|
const body = JSON.stringify(built.payload);
|
|
1939
2071
|
debugProviderDiagnostic("kiro", "request", {
|
|
1940
2072
|
region,
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import type { IncomingMeta, ProviderAdapter } from "./base";
|
|
3
3
|
import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../types";
|
|
4
4
|
import { catalogModelSupportsReasoningSummaries } from "../codex/catalog";
|
|
5
|
+
import { applyCodexRoutingHint, CODEX_RESPONSES_LITE_HEADER, CODEX_ROUTING_HINT_HEADER } from "../codex/forward-transport-headers";
|
|
5
6
|
import { COMPACT_PROMPT, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../responses/compaction";
|
|
6
7
|
import { collectResponsesToolGroups } from "../responses/tool-groups";
|
|
7
8
|
import { isHostedToolUnsupportedForModel } from "../responses/hosted-tool-policy";
|
|
@@ -20,6 +21,7 @@ import { rewriteRoutedCustomToolsForUpstream } from "../responses/custom-tool-co
|
|
|
20
21
|
import { rewriteRoutedToolSearchForUpstream } from "../responses/tool-search-compat";
|
|
21
22
|
import { rewriteRoutedNamespaceToolsForUpstream } from "../responses/namespace-tool-compat";
|
|
22
23
|
import { openaiResponsesUrl } from "./openai-responses-url";
|
|
24
|
+
import { normalizeResponsesCodeMode } from "./responses-code-mode";
|
|
23
25
|
import { injectXaiResponsesXSearch, normalizeXaiResponsesWebSearch } from "./xai-web-search";
|
|
24
26
|
import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "./empty-tool-output-annotation";
|
|
25
27
|
import {
|
|
@@ -51,6 +53,7 @@ export const FORWARD_HEADERS = [
|
|
|
51
53
|
"x-oai-attestation",
|
|
52
54
|
"x-openai-subagent",
|
|
53
55
|
"x-responsesapi-include-timing-metrics",
|
|
56
|
+
CODEX_RESPONSES_LITE_HEADER,
|
|
54
57
|
];
|
|
55
58
|
|
|
56
59
|
/**
|
|
@@ -187,7 +190,7 @@ const CANONICAL_ONLY_TOOL_FIELDS: readonly { field: string; toolTypes?: Readonly
|
|
|
187
190
|
// OWNERSHIP: official OpenAI API-key traffic and unclassified gateways ACCEPT this field, so
|
|
188
191
|
// it is only stripped when the provider capability denies it (supportsOpenAiWebSearchToolFields
|
|
189
192
|
// === false), matching stripOpenAiOnlyWebSearchFields; see
|
|
190
|
-
// tests/responses-routed-web-search-fields.test.ts.
|
|
193
|
+
// tests/responses/responses-routed-web-search-fields.test.ts.
|
|
191
194
|
{ field: "external_web_access", toolTypes: new Set(["web_search", "web_search_preview"]), capabilityGated: true },
|
|
192
195
|
// Deferred-discovery marker. `activateDeferredTool` clears it only for tools a `tool_search_output`
|
|
193
196
|
// already loaded, so a still-deferred declaration — including one promoted out of a namespace
|
|
@@ -906,6 +909,61 @@ function toolOutputText(output: unknown): string {
|
|
|
906
909
|
}).filter(Boolean).join("\n");
|
|
907
910
|
}
|
|
908
911
|
|
|
912
|
+
/** True when an output can be losslessly represented as user-message content. */
|
|
913
|
+
function isRepairableToolOutput(output: unknown): output is string | Record<string, unknown>[] {
|
|
914
|
+
if (typeof output === "string") return true;
|
|
915
|
+
if (!Array.isArray(output)) return false;
|
|
916
|
+
return output.every(part => {
|
|
917
|
+
if (!isPlainObject(part)) return false;
|
|
918
|
+
if (typeof part.type !== "string") return false;
|
|
919
|
+
if (["output_text", "text", "input_text"].includes(part.type)) {
|
|
920
|
+
return typeof part.text === "string";
|
|
921
|
+
}
|
|
922
|
+
if (part.type === "refusal") return typeof part.refusal === "string";
|
|
923
|
+
if (part.type === "encrypted_content") return typeof part.encrypted_content === "string";
|
|
924
|
+
if (part.type !== "input_image") return false;
|
|
925
|
+
const imageUrl = part.image_url;
|
|
926
|
+
const fileId = part.file_id;
|
|
927
|
+
const imageUrlIsString = typeof imageUrl === "string";
|
|
928
|
+
const fileIdIsString = typeof fileId === "string";
|
|
929
|
+
const hasUsableSource = (imageUrlIsString && imageUrl.length > 0)
|
|
930
|
+
|| (fileIdIsString && fileId.length > 0);
|
|
931
|
+
const validSource = hasUsableSource
|
|
932
|
+
&& (part.image_url === undefined || imageUrlIsString)
|
|
933
|
+
&& (part.file_id === undefined || fileIdIsString);
|
|
934
|
+
const validDetail = part.detail === undefined
|
|
935
|
+
|| (typeof part.detail === "string"
|
|
936
|
+
&& ["auto", "low", "high", "original"].includes(part.detail));
|
|
937
|
+
return validSource && validDetail;
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** Convert orphaned tool output to user-message content without discarding valid images. */
|
|
942
|
+
function orphanedToolOutputContent(output: unknown, callId = ""): Record<string, unknown>[] {
|
|
943
|
+
const marker = `[tool output for ${callId || "unknown call"}]`;
|
|
944
|
+
if (typeof output !== "string" && !Array.isArray(output)) {
|
|
945
|
+
return [{ type: "input_text", text: marker }];
|
|
946
|
+
}
|
|
947
|
+
if (!Array.isArray(output)) {
|
|
948
|
+
return [{ type: "input_text", text: `${marker}\n${toolOutputText(output)}` }];
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const content: Record<string, unknown>[] = [{ type: "input_text", text: marker }];
|
|
952
|
+
for (const part of output) {
|
|
953
|
+
if (!isPlainObject(part)) continue;
|
|
954
|
+
if (part.type === "input_image") {
|
|
955
|
+
content.push(part);
|
|
956
|
+
} else if (part.type === "encrypted_content" && typeof part.encrypted_content === "string") {
|
|
957
|
+
content.push({ type: "input_text", text: "[encrypted content omitted]" });
|
|
958
|
+
} else if (typeof part.text === "string") {
|
|
959
|
+
content.push({ type: "input_text", text: part.text });
|
|
960
|
+
} else if (part.type === "refusal" && typeof part.refusal === "string") {
|
|
961
|
+
content.push({ type: "input_text", text: `[refusal] ${part.refusal}` });
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return content;
|
|
965
|
+
}
|
|
966
|
+
|
|
909
967
|
/** True when a Responses tool output item is present but carries no usable content. */
|
|
910
968
|
function isToolOutputEmpty(output: unknown): boolean {
|
|
911
969
|
if (typeof output === "string") return output.trim() === "";
|
|
@@ -940,6 +998,32 @@ function annotateEmptyResponsesToolOutputs(body: unknown, enabled: boolean): unk
|
|
|
940
998
|
return changed ? { ...body, input } : body;
|
|
941
999
|
}
|
|
942
1000
|
|
|
1001
|
+
/**
|
|
1002
|
+
* Preserve the text of structurally invalid tool-output items before they reach a strict
|
|
1003
|
+
* Responses parser. Stateful destinations may legitimately receive an output whose matching
|
|
1004
|
+
* call lives behind `previous_response_id`, so ordinary orphan repair cannot run universally.
|
|
1005
|
+
* A missing or empty `call_id`, however, cannot identify stored state on any destination.
|
|
1006
|
+
*/
|
|
1007
|
+
function repairUnidentifiedToolOutputItems(body: unknown): unknown {
|
|
1008
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
1009
|
+
let changed = false;
|
|
1010
|
+
const input = body.input.map(item => {
|
|
1011
|
+
if (!isPlainObject(item)
|
|
1012
|
+
|| (item.type !== "function_call_output" && item.type !== "custom_tool_call_output")
|
|
1013
|
+
|| (typeof item.call_id === "string" && item.call_id.length > 0)) {
|
|
1014
|
+
return item;
|
|
1015
|
+
}
|
|
1016
|
+
if (!isRepairableToolOutput(item.output)) return item;
|
|
1017
|
+
changed = true;
|
|
1018
|
+
return {
|
|
1019
|
+
type: "message",
|
|
1020
|
+
role: "user",
|
|
1021
|
+
content: orphanedToolOutputContent(item.output),
|
|
1022
|
+
};
|
|
1023
|
+
});
|
|
1024
|
+
return changed ? { ...body, input } : body;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
943
1027
|
/**
|
|
944
1028
|
* Repair a forward-mode input array whose continuation context was lost. When the replay
|
|
945
1029
|
* expansion misses (proxy restart, unrecorded prior turn), previous_response_id is stripped
|
|
@@ -1060,12 +1144,17 @@ function repairOrphanedInputItems(body: unknown, dropReasoning: boolean, synthes
|
|
|
1060
1144
|
flushPendingSyntheticOutputs();
|
|
1061
1145
|
const callId = typeof item.call_id === "string" ? item.call_id : "";
|
|
1062
1146
|
const paired = isFnOutput ? functionCallIds.has(callId) : customCallIds.has(callId);
|
|
1063
|
-
|
|
1147
|
+
const usableOutput = isRepairableToolOutput(item.output);
|
|
1148
|
+
// A known orphan call is still useful as a labeled user message even when its output is
|
|
1149
|
+
// incomplete. With no call id and no output, preserve the invalid item so validation fails
|
|
1150
|
+
// closed rather than pretending any tool result exists.
|
|
1151
|
+
const knownNullOutput = callId.length > 0 && item.output == null;
|
|
1152
|
+
if (!paired && (knownNullOutput || usableOutput)) {
|
|
1064
1153
|
changed = true;
|
|
1065
1154
|
repaired.push({
|
|
1066
1155
|
type: "message",
|
|
1067
1156
|
role: "user",
|
|
1068
|
-
content:
|
|
1157
|
+
content: orphanedToolOutputContent(item.output, callId),
|
|
1069
1158
|
});
|
|
1070
1159
|
continue;
|
|
1071
1160
|
}
|
|
@@ -1893,14 +1982,67 @@ function normalizeImageGenClientTools(body: unknown): unknown {
|
|
|
1893
1982
|
* carries a tool the upstream model 400s on. No-op (returns the original reference) when nothing
|
|
1894
1983
|
* matches, keeping the common path allocation-free.
|
|
1895
1984
|
*/
|
|
1896
|
-
function stripUnsupportedHostedTools(body: unknown): unknown {
|
|
1897
|
-
if (!isPlainObject(body)
|
|
1985
|
+
function stripUnsupportedHostedTools(body: unknown, provider: Pick<OcxProviderConfig, "baseUrl">): unknown {
|
|
1986
|
+
if (!isPlainObject(body)) return body;
|
|
1898
1987
|
const model = typeof body.model === "string" ? body.model : "";
|
|
1899
|
-
const
|
|
1900
|
-
const
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1988
|
+
const filterTools = (tools: unknown[]): unknown[] => {
|
|
1989
|
+
const filtered = tools.filter(t => {
|
|
1990
|
+
const type = isPlainObject(t) && typeof t.type === "string" ? t.type : undefined;
|
|
1991
|
+
return !type || !isHostedToolUnsupportedForModel(model, type, provider.baseUrl);
|
|
1992
|
+
});
|
|
1993
|
+
return filtered.length === tools.length ? tools : filtered;
|
|
1994
|
+
};
|
|
1995
|
+
|
|
1996
|
+
let next: Record<string, unknown> = body;
|
|
1997
|
+
let changed = false;
|
|
1998
|
+
if (Array.isArray(body.tools)) {
|
|
1999
|
+
const tools = filterTools(body.tools);
|
|
2000
|
+
if (tools !== body.tools) {
|
|
2001
|
+
next = { ...next, tools };
|
|
2002
|
+
changed = true;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
if (Array.isArray(body.input)) {
|
|
2006
|
+
let inputChanged = false;
|
|
2007
|
+
const input = body.input.map(item => {
|
|
2008
|
+
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
|
|
2009
|
+
const tools = filterTools(item.tools);
|
|
2010
|
+
if (tools === item.tools) return item;
|
|
2011
|
+
inputChanged = true;
|
|
2012
|
+
return { ...item, tools };
|
|
2013
|
+
});
|
|
2014
|
+
if (inputChanged) {
|
|
2015
|
+
next = { ...next, input };
|
|
2016
|
+
changed = true;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
const toolChoice = next.tool_choice;
|
|
2021
|
+
if (isPlainObject(toolChoice) && toolChoice.type === "allowed_tools" && Array.isArray(toolChoice.tools)) {
|
|
2022
|
+
const tools = filterTools(toolChoice.tools);
|
|
2023
|
+
if (tools !== toolChoice.tools) {
|
|
2024
|
+
next = { ...next, tool_choice: tools.length > 0 ? { ...toolChoice, tools } : "none" };
|
|
2025
|
+
changed = true;
|
|
2026
|
+
}
|
|
2027
|
+
} else if (
|
|
2028
|
+
isPlainObject(toolChoice)
|
|
2029
|
+
&& typeof toolChoice.type === "string"
|
|
2030
|
+
&& isHostedToolUnsupportedForModel(model, toolChoice.type, provider.baseUrl)
|
|
2031
|
+
) {
|
|
2032
|
+
next = { ...next, tool_choice: "none" };
|
|
2033
|
+
changed = true;
|
|
2034
|
+
} else if (changed && toolChoice === "required") {
|
|
2035
|
+
const hasDeclaredTools = (Array.isArray(next.tools) && next.tools.length > 0)
|
|
2036
|
+
|| (Array.isArray(next.input) && next.input.some(item =>
|
|
2037
|
+
isPlainObject(item)
|
|
2038
|
+
&& item.type === "additional_tools"
|
|
2039
|
+
&& Array.isArray(item.tools)
|
|
2040
|
+
&& item.tools.length > 0));
|
|
2041
|
+
if (!hasDeclaredTools) {
|
|
2042
|
+
next = { ...next, tool_choice: "none" };
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
return changed ? next : body;
|
|
1904
2046
|
}
|
|
1905
2047
|
|
|
1906
2048
|
/**
|
|
@@ -1966,10 +2108,10 @@ export function stripOpenAiOnlyWebSearchFields(body: unknown): unknown {
|
|
|
1966
2108
|
}
|
|
1967
2109
|
|
|
1968
2110
|
/**
|
|
1969
|
-
* Muse Spark ids whose Responses gateway refuses
|
|
2111
|
+
* Muse Spark ids whose Responses gateway refuses provider-specific fields on a plain
|
|
1970
2112
|
* `web_search` tool. Membership, not equality: 1.3 shipped 2026-09-02 as the
|
|
1971
2113
|
* same-shaped successor to 1.2 on the same Zen wire, and an equality check would
|
|
1972
|
-
* have let a Codex-emitted `web_search`
|
|
2114
|
+
* have let a Codex-emitted `web_search` body reach the
|
|
1973
2115
|
* gateway and come back 400 for every request the moment 1.3 was selected.
|
|
1974
2116
|
*/
|
|
1975
2117
|
const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set([
|
|
@@ -1977,26 +2119,51 @@ const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set([
|
|
|
1977
2119
|
"muse-spark-1.2-contributor",
|
|
1978
2120
|
]);
|
|
1979
2121
|
|
|
2122
|
+
const MUSE_SPARK_WEB_SEARCH_STRICT_RESPONSE_URLS = new Set([
|
|
2123
|
+
"https://opencode.ai/zen/v1/responses",
|
|
2124
|
+
"https://opencode.ai/zen/go/v1/responses",
|
|
2125
|
+
]);
|
|
2126
|
+
|
|
2127
|
+
const MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS = [
|
|
2128
|
+
"search_content_types",
|
|
2129
|
+
"indexed_web_access",
|
|
2130
|
+
] as const;
|
|
2131
|
+
|
|
1980
2132
|
/**
|
|
1981
|
-
* OpenCode Zen / Go Muse Spark Responses gateway refuses
|
|
1982
|
-
*
|
|
1983
|
-
*
|
|
1984
|
-
*
|
|
1985
|
-
*
|
|
1986
|
-
*
|
|
1987
|
-
* keeping the tool type and every other accepted option intact.
|
|
2133
|
+
* OpenCode Zen / Go Muse Spark Responses gateway refuses a short list of Codex
|
|
2134
|
+
* `web_search` fields. `web_search_preview` keeps its accepted shape, and Luna
|
|
2135
|
+
* remains untouched. Match the exact effective request URL; malformed, credentialed,
|
|
2136
|
+
* or parameterized destinations keep their original body instead of assuming this
|
|
2137
|
+
* gateway contract. Keep the rejected names together so a newly identified field is
|
|
2138
|
+
* a one-line compatibility update rather than another bespoke rewrite.
|
|
1988
2139
|
*/
|
|
1989
|
-
function stripMuseSparkUnsupportedWebSearchFields(
|
|
2140
|
+
function stripMuseSparkUnsupportedWebSearchFields(
|
|
2141
|
+
body: unknown,
|
|
2142
|
+
modelId: unknown,
|
|
2143
|
+
responseUrl: string,
|
|
2144
|
+
): unknown {
|
|
1990
2145
|
if (!isPlainObject(body)) return body;
|
|
1991
2146
|
if (typeof modelId !== "string") return body;
|
|
1992
2147
|
if (!MUSE_SPARK_WEB_SEARCH_STRICT_MODELS.has(modelId.trim().toLowerCase())) return body;
|
|
2148
|
+
let destination: string;
|
|
2149
|
+
try {
|
|
2150
|
+
const url = new URL(responseUrl);
|
|
2151
|
+
if (url.username || url.password || url.search || url.hash) return body;
|
|
2152
|
+
destination = `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`;
|
|
2153
|
+
} catch {
|
|
2154
|
+
return body;
|
|
2155
|
+
}
|
|
2156
|
+
if (!MUSE_SPARK_WEB_SEARCH_STRICT_RESPONSE_URLS.has(destination)) return body;
|
|
1993
2157
|
|
|
1994
2158
|
const rewriteTools = (tools: unknown[]): { tools: unknown[]; changed: boolean } => {
|
|
1995
2159
|
let changed = false;
|
|
1996
2160
|
const rewritten = tools.map(tool => {
|
|
1997
2161
|
if (!isPlainObject(tool) || tool.type !== "web_search") return tool;
|
|
1998
|
-
if (!Object.hasOwn(tool,
|
|
1999
|
-
|
|
2162
|
+
if (!MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS.some(field => Object.hasOwn(tool, field))) {
|
|
2163
|
+
return tool;
|
|
2164
|
+
}
|
|
2165
|
+
const rest = { ...tool };
|
|
2166
|
+
for (const field of MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS) delete rest[field];
|
|
2000
2167
|
changed = true;
|
|
2001
2168
|
return rest;
|
|
2002
2169
|
});
|
|
@@ -2075,11 +2242,26 @@ function usageFromResponsesPayload(payload: unknown): OcxUsage | undefined {
|
|
|
2075
2242
|
const usage = payload.usage;
|
|
2076
2243
|
const inputTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
|
|
2077
2244
|
const outputTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
|
|
2078
|
-
|
|
2245
|
+
// openai/codex#41980: the raw usage object is wire data a rebuilt response.completed must keep —
|
|
2246
|
+
// unknown keys (subscription metadata, future counters) ride along even when the token counts
|
|
2247
|
+
// themselves are zero or absent (metadata-only usage).
|
|
2248
|
+
const knownKeys = new Set(["input_tokens", "output_tokens", "total_tokens", "input_tokens_details", "output_tokens_details"]);
|
|
2249
|
+
const hasExtras = Object.keys(usage).some(key => !knownKeys.has(key))
|
|
2250
|
+
|| (isPlainObject(usage.input_tokens_details)
|
|
2251
|
+
&& Object.keys(usage.input_tokens_details).some(key => key !== "cached_tokens" && key !== "cache_write_tokens"))
|
|
2252
|
+
|| (isPlainObject(usage.output_tokens_details)
|
|
2253
|
+
&& Object.keys(usage.output_tokens_details).some(key => key !== "reasoning_tokens"));
|
|
2254
|
+
if (inputTokens === 0 && outputTokens === 0 && !hasExtras) return undefined;
|
|
2255
|
+
const inputDetails = isPlainObject(usage.input_tokens_details) ? usage.input_tokens_details : undefined;
|
|
2256
|
+
const outputDetails = isPlainObject(usage.output_tokens_details) ? usage.output_tokens_details : undefined;
|
|
2079
2257
|
return {
|
|
2080
2258
|
inputTokens,
|
|
2081
2259
|
outputTokens,
|
|
2082
2260
|
...(typeof usage.total_tokens === "number" ? { totalTokens: usage.total_tokens } : {}),
|
|
2261
|
+
...(typeof inputDetails?.cached_tokens === "number" ? { cachedInputTokens: inputDetails.cached_tokens } : {}),
|
|
2262
|
+
...(typeof inputDetails?.cache_write_tokens === "number" ? { cacheCreationInputTokens: inputDetails.cache_write_tokens } : {}),
|
|
2263
|
+
...(typeof outputDetails?.reasoning_tokens === "number" ? { reasoningOutputTokens: outputDetails.reasoning_tokens } : {}),
|
|
2264
|
+
...(hasExtras ? { rawUsage: { ...usage } } : {}),
|
|
2083
2265
|
};
|
|
2084
2266
|
}
|
|
2085
2267
|
|
|
@@ -2137,7 +2319,14 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
2137
2319
|
if (mayForwardCallerCredentials) {
|
|
2138
2320
|
for (const h of FORWARD_HEADERS) {
|
|
2139
2321
|
const v = incoming?.headers.get(h);
|
|
2140
|
-
if (v)
|
|
2322
|
+
if (v) {
|
|
2323
|
+
if (h === CODEX_RESPONSES_LITE_HEADER) {
|
|
2324
|
+
for (const name of Object.keys(headers)) {
|
|
2325
|
+
if (name.toLowerCase() === h) delete headers[name];
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
headers[h] = v; // …so genuine forwarded fields win.
|
|
2329
|
+
}
|
|
2141
2330
|
}
|
|
2142
2331
|
}
|
|
2143
2332
|
const override = runtimeProvider._codexAccountOverride;
|
|
@@ -2250,18 +2439,22 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
2250
2439
|
if (provider.supportsOpenAiWebSearchToolFields === false) {
|
|
2251
2440
|
outBody = stripOpenAiOnlyWebSearchFields(outBody);
|
|
2252
2441
|
}
|
|
2253
|
-
outBody = stripMuseSparkUnsupportedWebSearchFields(outBody, parsed.modelId);
|
|
2442
|
+
outBody = stripMuseSparkUnsupportedWebSearchFields(outBody, parsed.modelId, url);
|
|
2254
2443
|
// Last, so promoted namespace children are also cleared of Codex-private fields.
|
|
2255
2444
|
outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false);
|
|
2256
2445
|
}
|
|
2257
2446
|
// Same predicate as the routedCompaction gate in handleResponses(): an authMode check would
|
|
2258
2447
|
// let a noncanonical custom forward provider skip this rewrite while the server still routes
|
|
2259
2448
|
// it as a summarizer turn (#422). The compaction body build removes the tool surface and must
|
|
2260
|
-
// therefore be the last routed transform
|
|
2261
|
-
//
|
|
2449
|
+
// therefore be the last routed transform that may depend on those declarations. Structural
|
|
2450
|
+
// sanitizers below can still run after it.
|
|
2451
|
+
outBody = normalizeResponsesCodeMode(outBody, parsed, provider);
|
|
2262
2452
|
if (parsed._compactionRequest === true && !isCanonicalOpenAiForwardProvider(provider)) {
|
|
2263
2453
|
outBody = buildRoutedCompactionBody(outBody);
|
|
2264
2454
|
}
|
|
2455
|
+
// Run after routed compaction so nested input_image parts are replaced before a malformed
|
|
2456
|
+
// tool output is flattened to text and can no longer be inspected structurally.
|
|
2457
|
+
outBody = repairUnidentifiedToolOutputItems(outBody);
|
|
2265
2458
|
const threadServingIdentityChanged = parsed._stripReasoningEncryptedContent === true;
|
|
2266
2459
|
const sanitizedBody = normalizeToolSchemas(
|
|
2267
2460
|
stripSparkCompatibility(
|
|
@@ -2281,6 +2474,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
2281
2474
|
stripEncryptedContent: threadServingIdentityChanged,
|
|
2282
2475
|
},
|
|
2283
2476
|
),
|
|
2477
|
+
provider,
|
|
2284
2478
|
),
|
|
2285
2479
|
),
|
|
2286
2480
|
),
|
|
@@ -2297,6 +2491,17 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
2297
2491
|
provider,
|
|
2298
2492
|
parsed.modelId,
|
|
2299
2493
|
);
|
|
2494
|
+
if (isCanonicalOpenAiForwardProvider(provider)) {
|
|
2495
|
+
const routingHeaders = new Headers(headers);
|
|
2496
|
+
applyCodexRoutingHint(routingHeaders, finalBody);
|
|
2497
|
+
// Static headers may use mixed casing. Remove every stale spelling
|
|
2498
|
+
// without normalizing unrelated headers returned by this adapter.
|
|
2499
|
+
for (const name of Object.keys(headers)) {
|
|
2500
|
+
if (name.toLowerCase() === CODEX_ROUTING_HINT_HEADER) delete headers[name];
|
|
2501
|
+
}
|
|
2502
|
+
const hint = routingHeaders.get(CODEX_ROUTING_HINT_HEADER);
|
|
2503
|
+
if (hint !== null) headers[CODEX_ROUTING_HINT_HEADER] = hint;
|
|
2504
|
+
}
|
|
2300
2505
|
const actualServiceTier = isPlainObject(finalBody) && typeof finalBody.service_tier === "string"
|
|
2301
2506
|
? finalBody.service_tier
|
|
2302
2507
|
: null;
|
|
@@ -2391,7 +2596,26 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
2391
2596
|
reservation.commitRetained();
|
|
2392
2597
|
budget.releaseRetained(previousBytes, { kind: "retained_collectors" });
|
|
2393
2598
|
}
|
|
2394
|
-
|
|
2599
|
+
{
|
|
2600
|
+
const nextUsage = usageFromResponsesPayload(payload.response);
|
|
2601
|
+
// The attached raw usage object can be event-sized (unknown keys carry arbitrary
|
|
2602
|
+
// values); it stays reachable until the terminal yields, so charge it like the
|
|
2603
|
+
// adjacent retained collectors or it would defeat the per-request memory cap.
|
|
2604
|
+
const previousRawBytes = usage?.rawUsage === undefined ? 0
|
|
2605
|
+
: budgetEncoder.encode(JSON.stringify(usage.rawUsage)).byteLength;
|
|
2606
|
+
const nextRawBytes = nextUsage?.rawUsage === undefined ? 0
|
|
2607
|
+
: budgetEncoder.encode(JSON.stringify(nextUsage.rawUsage)).byteLength;
|
|
2608
|
+
if (nextRawBytes > 0) {
|
|
2609
|
+
const reservation = budget.reserveTransient(nextRawBytes, { kind: "retained_collectors" });
|
|
2610
|
+
usage = nextUsage;
|
|
2611
|
+
reservation.commitRetained();
|
|
2612
|
+
} else {
|
|
2613
|
+
usage = nextUsage;
|
|
2614
|
+
}
|
|
2615
|
+
if (previousRawBytes > 0) {
|
|
2616
|
+
budget.releaseRetained(previousRawBytes, { kind: "retained_collectors" });
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2395
2619
|
break;
|
|
2396
2620
|
}
|
|
2397
2621
|
}
|
|
@@ -2399,7 +2623,13 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
2399
2623
|
// completed snapshot so text is never double-counted.
|
|
2400
2624
|
const text = snapshot || doneText || deltas;
|
|
2401
2625
|
if (text) yield { type: "text_delta", text };
|
|
2402
|
-
budget.releaseRetained(
|
|
2626
|
+
budget.releaseRetained(
|
|
2627
|
+
budgetEncoder.encode(deltas).byteLength
|
|
2628
|
+
+ budgetEncoder.encode(doneText).byteLength
|
|
2629
|
+
+ budgetEncoder.encode(snapshot).byteLength
|
|
2630
|
+
+ (usage?.rawUsage === undefined ? 0 : budgetEncoder.encode(JSON.stringify(usage.rawUsage)).byteLength),
|
|
2631
|
+
{ kind: "retained_collectors" },
|
|
2632
|
+
);
|
|
2403
2633
|
yield {
|
|
2404
2634
|
type: "done",
|
|
2405
2635
|
...(usage ? { usage } : {}),
|