@bitkyc08/opencodex 2.67.0-preview.20260926 → 2.68.0-preview.20260927
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 +3 -0
- package/gui/dist/assets/App-B5-cAEd2.js +51 -0
- package/gui/dist/assets/App-DCBismRi.css +1 -0
- package/gui/dist/assets/Tray-DFnMiyD0.js +1 -0
- package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
- package/gui/dist/assets/index-BcXblvem.js +86 -0
- package/gui/dist/assets/quota-summary-i84eOU3o.js +1 -0
- package/gui/dist/index.html +21 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-guard.ts +13 -1
- package/src/adapters/anthropic-image-normalize.ts +3 -2
- package/src/adapters/anthropic-output-schema.ts +36 -0
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/codebuddy/live-models.ts +180 -0
- package/src/adapters/coding-agent/protocol.ts +91 -12
- package/src/adapters/coding-agent/turn.ts +32 -27
- package/src/adapters/devin/cloud-direct/index.ts +1 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
- package/src/adapters/devin.ts +9 -6
- package/src/adapters/google-http.ts +4 -4
- package/src/adapters/google.ts +93 -3
- package/src/adapters/kiro/adapter.ts +6 -2
- package/src/adapters/kiro/stream.ts +12 -2
- package/src/adapters/kiro/usage.ts +3 -1
- package/src/adapters/kiro-errors.ts +11 -1
- package/src/adapters/kiro-events.ts +27 -1
- package/src/adapters/kiro-refusal.ts +30 -0
- package/src/adapters/kiro-retry.ts +71 -31
- package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
- package/src/adapters/openai-chat/messages.ts +31 -4
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
- package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
- package/src/adapters/openai-chat/tool-schema.ts +6 -2
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -3
- package/src/adapters/opencode-go-additional-tools.ts +19 -10
- package/src/adapters/physical-send.ts +10 -3
- package/src/adapters/registry.ts +2 -1
- package/src/adapters/responses-tool-schema.ts +31 -3
- package/src/adapters/run-turn-queue.ts +63 -23
- package/src/adapters/unique-tool-call-ids.ts +63 -0
- package/src/adapters/xai-web-search.ts +32 -2
- package/src/bridge/sse.ts +14 -2
- package/src/chat/inbound.ts +20 -6
- package/src/claude/agents-inject.ts +4 -0
- package/src/claude/desktop-3p.ts +53 -13
- package/src/claude/desktop-profile.ts +41 -4
- package/src/claude/inbound-model-options.ts +14 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +17 -1
- package/src/claude/intercept/local-ca.ts +7 -1
- package/src/claude/message-threads.ts +28 -0
- package/src/cli/account-api.ts +13 -0
- package/src/cli/account-auth.ts +49 -2
- package/src/cli/account-extended.ts +4 -2
- package/src/cli/account.ts +4 -2
- package/src/cli/capabilities.ts +17 -0
- package/src/cli/claude.ts +3 -1
- package/src/cli/dispatch.ts +62 -5
- package/src/cli/index.ts +84 -79
- package/src/cli/minimax.ts +4 -2
- package/src/cli/opencode.ts +6 -1
- package/src/cli/restart-handoff.ts +110 -0
- package/src/cli/status.ts +51 -0
- package/src/client/connect.ts +34 -15
- package/src/client/link-ingress.ts +102 -0
- package/src/client/link-join.ts +37 -16
- package/src/client/link-relay.ts +228 -45
- package/src/client/link-state.ts +54 -0
- package/src/client/link-status.ts +39 -0
- package/src/client/link-teardown.ts +2 -2
- package/src/client/link-tunnel.ts +615 -86
- package/src/client/machine-api.ts +10 -4
- package/src/client/machine-listener.ts +57 -10
- package/src/client/runtime.ts +210 -35
- package/src/clients/config-export/constants.ts +1 -12
- package/src/clients/config-export/contracts.ts +2 -0
- package/src/clients/config-export/model-metadata.ts +9 -3
- package/src/clients/config-export/omp.ts +1 -1
- package/src/clients/config-export/zcode-store.ts +2 -2
- package/src/clients/config-export.ts +36 -3
- package/src/codex/app-server-processes.ts +32 -0
- package/src/codex/app-server-restart-service.ts +20 -3
- package/src/codex/catalog/access-programs.ts +50 -0
- package/src/codex/catalog/build-entries.ts +13 -0
- package/src/codex/catalog/derive-entry.ts +1 -0
- package/src/codex/catalog/model-hints.ts +11 -0
- package/src/codex/catalog/parsing.ts +6 -2
- package/src/codex/catalog/provider-models.ts +81 -0
- package/src/codex/catalog/retained-sync.ts +3 -0
- package/src/codex/codex-write-lock.ts +2 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desired-state.ts +14 -4
- package/src/codex/home.ts +21 -3
- package/src/codex/inject/restore.ts +17 -0
- package/src/codex/inject/routing-classify.ts +3 -2
- package/src/codex/inject-coordination.ts +4 -1
- package/src/codex/inject.ts +8 -1
- package/src/codex/journal.ts +6 -2
- package/src/codex/management-convergence.ts +9 -0
- package/src/codex/model-entitlements.ts +28 -5
- package/src/codex/routing/idle-window.ts +58 -0
- package/src/codex/routing-drift.ts +134 -0
- package/src/codex/routing-healer.ts +419 -0
- package/src/codex/routing.ts +10 -0
- package/src/codex/runtime.ts +188 -57
- package/src/codex/sibling-handoff.ts +71 -0
- package/src/codex/sibling-start.ts +139 -0
- package/src/codex/sync.ts +5 -3
- package/src/combos/failover.ts +32 -5
- package/src/combos/request.ts +11 -3
- package/src/combos/reset-window.ts +10 -5
- package/src/combos/resolve.ts +36 -6
- package/src/config/diagnostics.ts +2 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/paths.ts +7 -1
- package/src/config/process-state.ts +10 -1
- package/src/config/schema/compaction-recovery.ts +15 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +9 -3
- package/src/generated/compatibility-version.json +360 -204
- package/src/images/loop.ts +47 -32
- package/src/integrations/catalog-refresh.ts +4 -0
- package/src/integrations/omp-yaml-source.ts +1 -0
- package/src/lib/claude-request-projection.ts +101 -0
- package/src/lib/local-account-switch-capability.ts +48 -0
- package/src/lib/local-upstream.ts +146 -0
- package/src/lib/package-tree-integrity.ts +25 -3
- package/src/lib/package-tree-retarget.ts +130 -0
- package/src/lib/process-control.ts +4 -1
- package/src/lib/system-restart-contract.ts +106 -0
- package/src/lib/upstream-retry.ts +57 -1
- package/src/link/ports.ts +9 -0
- package/src/link/ssh-argv.ts +16 -0
- package/src/link/ssh-runner.ts +70 -2
- package/src/link/tunnel-state.ts +58 -9
- package/src/oauth/account-quota-rank.ts +29 -18
- package/src/oauth/generic-account-failover.ts +179 -22
- package/src/oauth/index.ts +17 -4
- package/src/oauth/kiro-account-load.ts +107 -0
- package/src/oauth/kiro-device-login.ts +312 -0
- package/src/oauth/kiro-terminal-failover.ts +28 -0
- package/src/oauth/login-flow-state.ts +7 -1
- package/src/oauth/pool-settings-capability.ts +23 -14
- package/src/oauth/store.ts +56 -3
- package/src/oauth/types.ts +4 -0
- package/src/plugins/loader.ts +350 -0
- package/src/plugins/upstream-hooks.ts +119 -0
- package/src/protocols/encoders/adapter-events.ts +9 -2
- package/src/providers/account-quota-disk.ts +42 -4
- package/src/providers/codebuddy-models.ts +6 -2
- package/src/providers/command-code-efforts.ts +33 -10
- package/src/providers/kiro-account-state-disk.ts +44 -0
- package/src/providers/kiro-model-catalog.ts +162 -0
- package/src/providers/kiro-models.ts +5 -4
- package/src/providers/kiro-quota-metrics.ts +35 -0
- package/src/providers/kiro-usage.ts +118 -14
- package/src/providers/quota/account-cache.ts +43 -7
- package/src/providers/quota/antigravity.ts +8 -3
- package/src/providers/quota/kiro-account-probe.ts +12 -0
- package/src/providers/quota/vendor-probes-key.ts +31 -26
- package/src/providers/quota/vendor-probes-oauth.ts +16 -7
- package/src/providers/quota-types.ts +14 -0
- package/src/providers/quota.ts +21 -21
- package/src/providers/registry/entries-extended.ts +5 -3
- package/src/providers/request-pacing.ts +410 -12
- package/src/remote-control/workspace-codex-runtime.ts +4 -3
- package/src/responses/citation-markers.ts +132 -65
- package/src/responses/hosted-tool-policy.ts +14 -3
- package/src/responses/parser-content.ts +3 -2
- package/src/responses/parser.ts +4 -1
- package/src/responses/schema.ts +4 -0
- package/src/responses/visualization-directives.ts +182 -0
- package/src/server/chat-native-sse.ts +24 -12
- package/src/server/chat-native.ts +137 -68
- package/src/server/claude-messages.ts +147 -12
- package/src/server/index/link-listener.ts +7 -0
- package/src/server/index/optional-listeners.ts +4 -1
- package/src/server/index/package-tree-guard.ts +35 -16
- package/src/server/index/serve-options.ts +4 -2
- package/src/server/index/startup-warnings.ts +6 -0
- package/src/server/index.ts +7 -7
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/inference/context.ts +30 -2
- package/src/server/local-account-switch-auth.ts +79 -0
- package/src/server/management/agent-settings-routes.ts +11 -3
- package/src/server/management/config-routes.ts +28 -11
- package/src/server/management/context.ts +2 -0
- package/src/server/management/link-routes.ts +123 -39
- package/src/server/management/logs-usage-routes.ts +9 -42
- package/src/server/management/model-rows.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +67 -12
- package/src/server/management/route-registry.ts +5 -5
- package/src/server/management/sibling-guard.ts +60 -0
- package/src/server/management/storage-log-guard-routes.ts +72 -6
- package/src/server/management/system-restart.ts +92 -135
- package/src/server/management/system-routes.ts +1 -1
- package/src/server/management-api.ts +31 -3
- package/src/server/management-auth.ts +3 -0
- package/src/server/port-reclaim.ts +111 -1
- package/src/server/proxy-liveness.ts +16 -0
- package/src/server/relay.ts +16 -15
- package/src/server/request-log-filter.ts +28 -27
- package/src/server/request-log.ts +7 -3
- package/src/server/request-metrics.ts +28 -0
- package/src/server/responses/adapter-continuation.ts +151 -21
- package/src/server/responses/adapter-delivery.ts +21 -7
- package/src/server/responses/adapter-dispatch.ts +239 -51
- package/src/server/responses/codex-ws-pool.ts +5 -4
- package/src/server/responses/codex-ws-request.ts +4 -1
- package/src/server/responses/compact.ts +8 -4
- package/src/server/responses/compaction-recovery-policy.ts +116 -0
- package/src/server/responses/compaction-recovery.ts +330 -0
- package/src/server/responses/core-combo-failure.ts +1 -0
- package/src/server/responses/core-combo.ts +5 -1
- package/src/server/responses/core-lifetime.ts +22 -0
- package/src/server/responses/core-options.ts +16 -1
- package/src/server/responses/core.ts +14 -4
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +82 -31
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +12 -2
- package/src/server/responses/passthrough-execution.ts +2 -2
- package/src/server/responses/request-send-budget.ts +21 -5
- package/src/server/responses/request-transport.ts +116 -18
- package/src/server/responses/reset-replay.ts +32 -0
- package/src/server/responses/run-turn-execution.ts +136 -19
- package/src/server/responses/sidecar-execution.ts +41 -8
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses/ws-upstream.ts +44 -4
- package/src/server/restart-replacement.ts +382 -0
- package/src/server/startup-health-cache.ts +23 -4
- package/src/server/stop-teardown.ts +35 -1
- package/src/server/system-env.ts +3 -0
- package/src/service/windows-taskxml.ts +11 -7
- package/src/service/windows-wrapper-exit.ts +8 -0
- package/src/stall-timeout.ts +35 -10
- package/src/storage/policy-job.ts +4 -0
- package/src/storage/scanner.ts +177 -29
- package/src/storage/storage-mutation-coordinator.ts +17 -0
- package/src/tray/windows-tray.ps1 +50 -23
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +5 -1
- package/src/types/request.ts +13 -2
- package/src/update/index.ts +3 -1
- package/src/update/job.ts +6 -2
- package/src/update/mise-launcher-target.ts +100 -0
- package/src/update/worker-launch.ts +46 -0
- package/src/usage/log.ts +2 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-B3VRRYSj.js +0 -51
- package/gui/dist/assets/App-BJsT8Icc.css +0 -1
- package/gui/dist/assets/Tray-TYAA0EQY.js +0 -1
- package/gui/dist/assets/index-9Ju7nHsu.js +0 -86
- package/gui/dist/assets/tray-data-xKfS2gSG.js +0 -1
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
retryBackoffDelayMs,
|
|
15
15
|
sleepWithAbort,
|
|
16
16
|
} from "../lib/upstream-retry";
|
|
17
|
+
import { releaseProviderRequestSlot, sendTrackingRequestSlot, type ProviderRequestSlot } from "../providers/request-pacing";
|
|
17
18
|
|
|
18
19
|
const RESET_ATTEMPTS = 3;
|
|
19
20
|
const RESET_RETRY_BASE_MS = 150;
|
|
@@ -29,6 +30,7 @@ const ENDPOINT_ERROR_MARKERS = [
|
|
|
29
30
|
"unsupported endpoint",
|
|
30
31
|
];
|
|
31
32
|
const CONNECT_ERROR_CODES = new Set(["ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED", "ENETUNREACH", "EHOSTUNREACH"]);
|
|
33
|
+
type KiroFetchContext = AdapterFetchContext & { allowGatewayRotation?: boolean };
|
|
32
34
|
|
|
33
35
|
interface KiroThrottleProbe {
|
|
34
36
|
token: symbol;
|
|
@@ -165,33 +167,54 @@ async function fetchWithResetRecovery(
|
|
|
165
167
|
let lastError: unknown;
|
|
166
168
|
for (let attempt = 0; attempt < RESET_ATTEMPTS; attempt++) {
|
|
167
169
|
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
throw new SendBudgetExhaustedError(url);
|
|
174
|
-
}
|
|
175
|
-
// Reported after admission and before dispatch, so a refused send is never counted and an
|
|
176
|
-
// admitted one is counted exactly once whichever way the fetch below settles.
|
|
177
|
-
notePhysicalSend(attempt > 0);
|
|
170
|
+
const executor = (ctx.executor ?? globalThis.fetch) as typeof globalThis.fetch & {
|
|
171
|
+
waitForPacing?: (signal?: AbortSignal) => Promise<ProviderRequestSlot | void>;
|
|
172
|
+
unpacedFetch?: typeof globalThis.fetch;
|
|
173
|
+
};
|
|
174
|
+
const slot = (await executor.waitForPacing?.(ctx.abortSignal)) || undefined;
|
|
178
175
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
176
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
177
|
+
// Every physical send is admitted, not just the adapter entry. Kiro nests a throttle loop
|
|
178
|
+
// over this ladder and can run the ladder twice per throttle round, so counting one entry
|
|
179
|
+
// as one send hid up to eighteen upstream requests from the per-request cap (#4546).
|
|
180
|
+
const decision = ctx.sendBudget?.reserveDispatch({ sendClass: "transient", targetKey: url });
|
|
181
|
+
if (decision && (!decision.allowed || !decision.permit.use())) {
|
|
182
|
+
throw new SendBudgetExhaustedError(url);
|
|
183
|
+
}
|
|
184
|
+
// Reported after admission and before dispatch, so a refused send is never counted and an
|
|
185
|
+
// admitted one is counted exactly once whichever way the fetch below settles.
|
|
186
|
+
notePhysicalSend(attempt > 0);
|
|
187
|
+
try {
|
|
188
|
+
const headers = new Headers(request.headers);
|
|
189
|
+
const recovered = attempt > 0;
|
|
190
|
+
if (recovered) headers.set("connection", "close");
|
|
191
|
+
return await sendTrackingRequestSlot(slot, () => fetchWithAttemptDeadline(url, {
|
|
192
|
+
method: request.method,
|
|
193
|
+
headers,
|
|
194
|
+
body: request.body,
|
|
195
|
+
...(recovered ? { keepalive: false } : {}),
|
|
196
|
+
}, timeoutMs, ctx.abortSignal, ctx.stream, (async (input, init) => {
|
|
197
|
+
try {
|
|
198
|
+
return await (executor.unpacedFetch ?? executor)(input, init);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
201
|
+
const signal = init?.signal;
|
|
202
|
+
if (signal?.aborted && signal.reason instanceof Error && signal.reason.name === "TimeoutError") {
|
|
203
|
+
throw signal.reason;
|
|
204
|
+
}
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}) as typeof globalThis.fetch));
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (ctx.abortSignal?.aborted || !isConnectionResetError(error) || attempt === RESET_ATTEMPTS - 1) throw error;
|
|
210
|
+
lastError = error;
|
|
211
|
+
await sleepWithAbort(retryBackoffDelayMs(attempt, {
|
|
212
|
+
baseDelayMs: RESET_RETRY_BASE_MS,
|
|
213
|
+
maxDelayMs: RESET_RETRY_MAX_MS,
|
|
214
|
+
}), ctx.abortSignal);
|
|
215
|
+
}
|
|
216
|
+
} finally {
|
|
217
|
+
releaseProviderRequestSlot(slot);
|
|
195
218
|
}
|
|
196
219
|
}
|
|
197
220
|
throw lastError ?? new Error("Kiro fetch failed");
|
|
@@ -217,7 +240,7 @@ async function inspectEndpointHttpFailure(
|
|
|
217
240
|
return { response: rebuilt, fallback: ENDPOINT_ERROR_MARKERS.some(marker => text.includes(marker)) };
|
|
218
241
|
}
|
|
219
242
|
|
|
220
|
-
async function normalizeFinalKiroHttpError(res: Response, signal?: AbortSignal): Promise<Response> {
|
|
243
|
+
export async function normalizeFinalKiroHttpError(res: Response, signal?: AbortSignal): Promise<Response> {
|
|
221
244
|
return normalizeUpstreamHttpErrorResponse(res, {
|
|
222
245
|
signal,
|
|
223
246
|
formatMessage: payloadText => safeKiroHttpErrorMessage(res.status, res.headers, payloadText),
|
|
@@ -255,19 +278,28 @@ async function inspectKiroThrottle(
|
|
|
255
278
|
|
|
256
279
|
async function fetchKiroAttempt(
|
|
257
280
|
request: AdapterRequest,
|
|
258
|
-
ctx:
|
|
281
|
+
ctx: KiroFetchContext,
|
|
259
282
|
timeoutMs: number,
|
|
260
283
|
notePhysicalSend: (reset: boolean) => void,
|
|
261
284
|
): Promise<Response> {
|
|
262
|
-
const
|
|
285
|
+
const plannedUrl = request.url;
|
|
286
|
+
const legacy = legacyUrl(plannedUrl);
|
|
263
287
|
let response: Response;
|
|
264
288
|
try {
|
|
265
|
-
response = await fetchWithResetRecovery(request,
|
|
289
|
+
response = await fetchWithResetRecovery(request, plannedUrl, ctx, timeoutMs, notePhysicalSend);
|
|
266
290
|
} catch (error) {
|
|
267
291
|
if (!legacy || !endpointConnectFailure(error)) throw error;
|
|
268
292
|
return fetchWithResetRecovery(request, legacy, ctx, timeoutMs, notePhysicalSend);
|
|
269
293
|
}
|
|
270
294
|
|
|
295
|
+
if (!response.ok && (response.status === 502 || response.status === 503 || response.status === 504)) {
|
|
296
|
+
const baseUrl = response.url || request.url || plannedUrl;
|
|
297
|
+
const alternate = ctx.allowGatewayRotation === false ? undefined : legacyUrl(baseUrl);
|
|
298
|
+
if (alternate) {
|
|
299
|
+
cancelResponseBodyBestEffort(response);
|
|
300
|
+
return fetchWithResetRecovery(request, alternate, ctx, timeoutMs, notePhysicalSend);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
271
303
|
if (legacy && !response.ok) {
|
|
272
304
|
const inspected = await inspectEndpointHttpFailure(response, ctx.abortSignal);
|
|
273
305
|
response = inspected.response;
|
|
@@ -284,7 +316,7 @@ async function fetchKiroAttempt(
|
|
|
284
316
|
* throttle recovery. The shared probe starts only after a 429, so healthy parallel traffic remains
|
|
285
317
|
* parallel while a throttled account cannot burn every caller's independent retry budget (#532).
|
|
286
318
|
*/
|
|
287
|
-
export async function fetchKiroWithRetry(request: AdapterRequest, ctx:
|
|
319
|
+
export async function fetchKiroWithRetry(request: AdapterRequest, ctx: KiroFetchContext = {}): Promise<Response> {
|
|
288
320
|
const timeoutMs = ctx.timeoutMs ?? 200_000;
|
|
289
321
|
let probeToken: symbol | undefined;
|
|
290
322
|
// One ordinal sequence for the whole call, across the throttle loop, the endpoint fallback
|
|
@@ -307,6 +339,10 @@ export async function fetchKiroWithRetry(request: AdapterRequest, ctx: AdapterFe
|
|
|
307
339
|
|
|
308
340
|
const response = await fetchKiroAttempt(request, ctx, timeoutMs, notePhysicalSend);
|
|
309
341
|
const throttle = await inspectKiroThrottle(response, ctx.abortSignal);
|
|
342
|
+
if (throttle && ctx.kiroPreferAccountFailover) {
|
|
343
|
+
releaseKiroThrottleProbe(probeToken);
|
|
344
|
+
return throttle.response;
|
|
345
|
+
}
|
|
310
346
|
if (!throttle || !throttle.transient) {
|
|
311
347
|
releaseKiroThrottleProbe(probeToken);
|
|
312
348
|
const finalResponse = throttle?.response ?? response;
|
|
@@ -334,6 +370,10 @@ export async function fetchKiroWithRetry(request: AdapterRequest, ctx: AdapterFe
|
|
|
334
370
|
throw new Error("Kiro throttle retry loop exhausted without a response");
|
|
335
371
|
} catch (error) {
|
|
336
372
|
releaseKiroThrottleProbe(probeToken);
|
|
373
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
374
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
375
|
+
return new Response("Kiro upstream gateway timeout", { status: 504 });
|
|
376
|
+
}
|
|
337
377
|
throw error;
|
|
338
378
|
}
|
|
339
379
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { OcxProviderConfig } from "../../types";
|
|
2
|
+
|
|
3
|
+
export function isDeepseekArtifactTarget(provider: OcxProviderConfig, name: string, namespace?: string): boolean {
|
|
4
|
+
if (name !== "Artifact" || namespace) return false;
|
|
5
|
+
try {
|
|
6
|
+
return new URL(provider.baseUrl).hostname === "api.deepseek.com";
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const NAME_BAGS = new Set(["properties", "$defs", "definitions", "patternProperties", "dependentSchemas"]);
|
|
13
|
+
const SCHEMA_CHILDREN = new Set(["items", "additionalProperties", "additionalItems", "allOf", "oneOf", "not", "if", "then", "else", "contains", "propertyNames", "prefixItems", "unevaluatedItems", "unevaluatedProperties"]);
|
|
14
|
+
|
|
15
|
+
/** DeepSeek rejects Artifact regex/union constraints. Relax them only on this built-in tool.
|
|
16
|
+
* Walk schema positions, preserving property names and literal defaults/examples verbatim.
|
|
17
|
+
* An explicit stack avoids recursion on caller-controlled schema nesting.
|
|
18
|
+
*/
|
|
19
|
+
export function relaxDeepseekArtifactSchema(schema: unknown): unknown {
|
|
20
|
+
let result: unknown;
|
|
21
|
+
const pending: Array<{ value: unknown; names?: boolean; assign: (value: unknown) => void }> = [
|
|
22
|
+
{ value: schema, assign: value => { result = value; } },
|
|
23
|
+
];
|
|
24
|
+
while (pending.length) {
|
|
25
|
+
const { value, names, assign } = pending.pop()!;
|
|
26
|
+
if (!value || typeof value !== "object") { assign(value); continue; }
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
const out: unknown[] = new Array(value.length);
|
|
29
|
+
assign(out);
|
|
30
|
+
value.forEach((child, index) => pending.push({ value: child, assign: next => { out[index] = next; } }));
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const out: Record<string, unknown> = Object.create(null);
|
|
34
|
+
assign(out);
|
|
35
|
+
for (const [key, child] of Object.entries(value)) {
|
|
36
|
+
if (!names && (key === "pattern" || key === "anyOf")) continue;
|
|
37
|
+
if (names || NAME_BAGS.has(key) || SCHEMA_CHILDREN.has(key)) {
|
|
38
|
+
pending.push({ value: child, names: !names && NAME_BAGS.has(key), assign: next => { out[key] = next; } });
|
|
39
|
+
} else {
|
|
40
|
+
out[key] = child;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
@@ -7,6 +7,7 @@ import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "../
|
|
|
7
7
|
import { identifyRoutedModel } from "../identity";
|
|
8
8
|
import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "../tool-catalog-nudge";
|
|
9
9
|
import { peekReasoningForCall } from "../../responses/reasoning-replay-cache";
|
|
10
|
+
import type { ClaudeThinkingProjection } from "../../lib/claude-request-projection";
|
|
10
11
|
import { inlineDocumentDataUrl } from "../../responses/inline-document";
|
|
11
12
|
import type { OcxAssistantMessage, OcxContentPart, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall } from "../../types";
|
|
12
13
|
import { modelInList, namespacedToolName } from "../../types";
|
|
@@ -61,10 +62,36 @@ export function toolResultImageChatParts(content: string | OcxContentPart[]): un
|
|
|
61
62
|
return parts;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Whether this wire serializes a replayed thinking block for the given model.
|
|
67
|
+
*
|
|
68
|
+
* The single source of truth for that question on the Chat wire: the assistant branch below uses
|
|
69
|
+
* it to decide `reasoning_content`, and the Messages ingress uses it to decide how much of a
|
|
70
|
+
* prompt is worth counting. Two copies of this rule would drift, and the count would then
|
|
71
|
+
* describe a body this adapter does not send.
|
|
72
|
+
*/
|
|
73
|
+
export function openAIChatSerializesThinking(
|
|
74
|
+
provider: OcxProviderConfig,
|
|
75
|
+
modelId: string,
|
|
76
|
+
): ClaudeThinkingProjection {
|
|
77
|
+
return {
|
|
78
|
+
text: modelInList(provider.preserveReasoningContentModels, modelId),
|
|
79
|
+
// The wire has no `signature` field — base64 replay tokens are Anthropic-wire-only.
|
|
80
|
+
signature: false,
|
|
81
|
+
// A `redacted_thinking` block is opaque provider data with no Chat representation: the
|
|
82
|
+
// assistant branch below reads only `type: "thinking"` parts, so the encrypted form is
|
|
83
|
+
// dropped whatever the preserve list says.
|
|
84
|
+
redacted: false,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
64
88
|
export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] {
|
|
65
89
|
const out: unknown[] = [];
|
|
66
90
|
const { context, options } = parsed;
|
|
67
91
|
const replayCacheScope = parsed._reasoningReplayScope;
|
|
92
|
+
// One question, one answer for the whole conversion: does this wire carry replayed thinking
|
|
93
|
+
// back for this model? Asked once so a long history cannot pay for it per message.
|
|
94
|
+
const wireSerializesThinking = openAIChatSerializesThinking(provider, parsed.modelId).text;
|
|
68
95
|
|
|
69
96
|
interface PendingToolCall { id: string; name: string }
|
|
70
97
|
let pendingToolCalls: PendingToolCall[] = [];
|
|
@@ -225,7 +252,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
225
252
|
if (
|
|
226
253
|
reasoningContent.length === 0
|
|
227
254
|
&& (toolCalls.length > 0 || thinkingParts.length > 0)
|
|
228
|
-
&&
|
|
255
|
+
&& wireSerializesThinking
|
|
229
256
|
) {
|
|
230
257
|
const cached = toolCalls
|
|
231
258
|
.map(tc => (tc.id ? peekReasoningForCall(tc.id, replayCacheScope) : undefined))
|
|
@@ -248,7 +275,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
248
275
|
reasoningContent = " ";
|
|
249
276
|
}
|
|
250
277
|
}
|
|
251
|
-
if (reasoningContent.length > 0 &&
|
|
278
|
+
if (reasoningContent.length > 0 && wireSerializesThinking) {
|
|
252
279
|
// MiniMax's interleaved-thinking contract requires the structured
|
|
253
280
|
// reasoning_details array back on the next turn; a reasoning_content
|
|
254
281
|
// string is the native-format pass-back the docs mark unsupported.
|
|
@@ -302,7 +329,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
302
329
|
flushPendingToolCalls();
|
|
303
330
|
const name = safeToolName(msg.toolName);
|
|
304
331
|
const cachedReasoning =
|
|
305
|
-
toolCallId &&
|
|
332
|
+
toolCallId && wireSerializesThinking
|
|
306
333
|
? peekReasoningForCall(toolCallId, replayCacheScope)
|
|
307
334
|
: undefined;
|
|
308
335
|
// Same fallback as the main-assistant path: never emit a bare orphan
|
|
@@ -316,7 +343,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
316
343
|
// falsy hit as a miss so the placeholder still fires.
|
|
317
344
|
const orphanReasoning =
|
|
318
345
|
cachedReasoning
|
|
319
|
-
|| (
|
|
346
|
+
|| (wireSerializesThinking
|
|
320
347
|
&& modelInList(provider.requiresReasoningPlaceholderModels ?? provider.preserveReasoningContentModels, parsed.modelId)
|
|
321
348
|
? " "
|
|
322
349
|
: undefined);
|
|
@@ -46,6 +46,7 @@ export interface StructuredToolCallReference {
|
|
|
46
46
|
names: ReadonlySet<string>;
|
|
47
47
|
argumentsText: string;
|
|
48
48
|
freeformTool?: FreeformToolIdentity;
|
|
49
|
+
recoveredParameterWrapper?: boolean;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
const BLOCK_HEADER = /<tool_call>\s*<function=([^>\r\n]+)>/y;
|
|
@@ -222,6 +223,7 @@ function deferredContinuationBreak(text: string, context: TextContext): RegExp |
|
|
|
222
223
|
* original order.
|
|
223
224
|
*/
|
|
224
225
|
export class SerializedToolCallContentBuffer {
|
|
226
|
+
releasedAnswerText = false;
|
|
225
227
|
private text = "";
|
|
226
228
|
private bytes = 0;
|
|
227
229
|
private hasOpenTag = false;
|
|
@@ -235,6 +237,12 @@ export class SerializedToolCallContentBuffer {
|
|
|
235
237
|
|
|
236
238
|
constructor(private readonly budget: TranslatorBudget) {}
|
|
237
239
|
|
|
240
|
+
/** Remembers visible text released before a later structured call can be reconciled. */
|
|
241
|
+
private observeText(events: AdapterEvent[]): AdapterEvent[] {
|
|
242
|
+
if (events.some(event => event.type === "text_delta" && event.text.trim() !== "")) this.releasedAnswerText = true;
|
|
243
|
+
return events;
|
|
244
|
+
}
|
|
245
|
+
|
|
238
246
|
/** Measures an append exactly even when the runtime prices isolated UTF-16 surrogates differently. */
|
|
239
247
|
private appendedByteLength(delta: string): number {
|
|
240
248
|
let bytes = Buffer.byteLength(delta);
|
|
@@ -324,16 +332,16 @@ export class SerializedToolCallContentBuffer {
|
|
|
324
332
|
// A delta that alone passes the bound is delivered as text rather than retained.
|
|
325
333
|
if (Buffer.byteLength(delta) > MAX_HELD_BYTES) {
|
|
326
334
|
this.context = contextAfter(delta, this.context);
|
|
327
|
-
return [...released, ...textEvents(delta)];
|
|
335
|
+
return this.observeText([...released, ...textEvents(delta)]);
|
|
328
336
|
}
|
|
329
|
-
return [...released, ...textEvents(this.ingest(delta))];
|
|
337
|
+
return this.observeText([...released, ...textEvents(this.ingest(delta))]);
|
|
330
338
|
}
|
|
331
339
|
const text = this.ingest(delta);
|
|
332
340
|
// Checked after ingest too: one delta can open a block and already carry more than a bound.
|
|
333
341
|
if (this.hasOpenTag && (this.bytes > MAX_HELD_BYTES || (this.sawCloser && !this.openAfterCloser && this.trailingProseChars > MAX_TRAILING_CHARS))) {
|
|
334
|
-
return [...textEvents(text), ...this.drain([])];
|
|
342
|
+
return this.observeText([...textEvents(text), ...this.drain([])]);
|
|
335
343
|
}
|
|
336
|
-
return textEvents(text);
|
|
344
|
+
return this.observeText(textEvents(text));
|
|
337
345
|
}
|
|
338
346
|
|
|
339
347
|
/** Exposes held text as evidence for narrowly repairing duplicated argument prefixes. */
|
|
@@ -386,7 +394,7 @@ export class SerializedToolCallContentBuffer {
|
|
|
386
394
|
this.context = contextAfter(this.text, this.context);
|
|
387
395
|
this.queued = [];
|
|
388
396
|
this.replace("", false);
|
|
389
|
-
return out;
|
|
397
|
+
return this.observeText(out);
|
|
390
398
|
}
|
|
391
399
|
|
|
392
400
|
/** Text-only drain for callers that never queued an event. */
|
|
@@ -475,7 +483,10 @@ function duplicatedSerializedToolCallRanges(
|
|
|
475
483
|
const body = freeformBody(call.body);
|
|
476
484
|
return structuredCalls.some(structured => {
|
|
477
485
|
const input = structured.names.has(call.name) ? inputFromArguments(structured.argumentsText, structured.freeformTool) : undefined;
|
|
478
|
-
|
|
486
|
+
if (input === undefined) return false;
|
|
487
|
+
const normalized = freeformBody(input);
|
|
488
|
+
return normalized === body || (structured.recoveredParameterWrapper === true
|
|
489
|
+
&& body.startsWith("<parameter=") && normalized === freeformBody(body.slice("<parameter=".length)));
|
|
479
490
|
});
|
|
480
491
|
});
|
|
481
492
|
}
|
|
@@ -578,14 +589,48 @@ export interface StructuredToolCallInput {
|
|
|
578
589
|
export function reconcileStructuredToolCalls(
|
|
579
590
|
calls: readonly StructuredToolCallInput[],
|
|
580
591
|
serializedText: string,
|
|
592
|
+
recoverEmptyMiMoFreeformInput = false,
|
|
581
593
|
): StructuredToolCallReference[] {
|
|
582
|
-
const references = calls.map(call => {
|
|
594
|
+
const references: StructuredToolCallReference[] = calls.map(call => {
|
|
583
595
|
const names = new Set([call.wireName, call.restoredName]);
|
|
584
596
|
return { names, freeformTool: call.freeformTool, argumentsText: repairArgumentsDuplicatedBesideSerializedCall(call.argumentsText, names, serializedText) };
|
|
585
597
|
});
|
|
598
|
+
if (recoverEmptyMiMoFreeformInput && calls.length === 1 && calls[0]!.freeformTool
|
|
599
|
+
&& emptyObjectArguments(references[0]!.argumentsText)) {
|
|
600
|
+
// A standalone MiMo block and one empty call to the exact wire tool provide the only
|
|
601
|
+
// unambiguous recovery shape. Prose before/after it or another call keeps text inert.
|
|
602
|
+
const blocks = callsIn(serializedText);
|
|
603
|
+
if (blocks.length === 1 && serializedText.slice(0, blocks[0]!.start).trim() === ""
|
|
604
|
+
&& serializedText.slice(blocks[0]!.end).trim() === "") {
|
|
605
|
+
const block = blocks[0]!;
|
|
606
|
+
const wrappedBody = freeformBody(block.body);
|
|
607
|
+
// MiMo can put the freeform input after a malformed `<parameter=` opener.
|
|
608
|
+
const hasParameterOpener = wrappedBody.startsWith("<parameter=");
|
|
609
|
+
const body = hasParameterOpener
|
|
610
|
+
? freeformBody(wrappedBody.slice("<parameter=".length)) : wrappedBody;
|
|
611
|
+
if (block.name === calls[0]!.wireName && body.trim() !== ""
|
|
612
|
+
&& !/(?:^|\n)<tool_call>\s*<function=[^>\r\n]+>/.test(block.body)
|
|
613
|
+
&& !(hasParameterOpener && /^<parameter=[A-Za-z_$][\w$.-]*>/.test(wrappedBody))
|
|
614
|
+
&& !body.includes("<parameter=") && !body.includes("</parameter>")) {
|
|
615
|
+
references[0] = { ...references[0]!, argumentsText: JSON.stringify({ input: body }),
|
|
616
|
+
recoveredParameterWrapper: hasParameterOpener };
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
586
620
|
return reduceUnambiguousDoubledInput(references, serializedText);
|
|
587
621
|
}
|
|
588
622
|
|
|
623
|
+
/** An empty JSON object means the structured freeform call supplied no input. */
|
|
624
|
+
function emptyObjectArguments(text: string): boolean {
|
|
625
|
+
try {
|
|
626
|
+
const parsed: unknown = JSON.parse(text);
|
|
627
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
628
|
+
&& Object.keys(parsed).length === 0;
|
|
629
|
+
} catch {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
589
634
|
/**
|
|
590
635
|
* Applies the doubled-input reduction across the batch. The doubled shape is valid JSON, so the
|
|
591
636
|
* prefix repair returns it untouched, and the reduction only ever rewrites a call the repair left
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { isConformingToolCallId, MAX_TOOL_CALL_ID_LENGTH } from "../tool-call-id";
|
|
2
|
+
import type { OcxMessage } from "../../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Remint a tool-call id the client's history already carries.
|
|
6
|
+
*
|
|
7
|
+
* Some upstreams mint tool-call ids deterministically per RESPONSE rather than per call: the same
|
|
8
|
+
* `call-0-0` on every turn of a conversation. Anthropic's wire requires `tool_use.id` to identify
|
|
9
|
+
* one call, and a client that has already stored that id for an earlier call cannot pair the new
|
|
10
|
+
* result to the new call — it drops one of them, the turn becomes an assistant message whose
|
|
11
|
+
* tool call has no result, and the model re-issues the same call forever.
|
|
12
|
+
*
|
|
13
|
+
* The first occurrence of an id is emitted byte-identical, so prompt-cache keys, reasoning-replay
|
|
14
|
+
* lookups, and every upstream that already mints unique ids stay untouched. Only a repeat — against
|
|
15
|
+
* the history the caller seeded, or against a call already emitted in this response — is rewritten,
|
|
16
|
+
* to the smallest unused suffix that still fits Anthropic's id bound.
|
|
17
|
+
*/
|
|
18
|
+
export function createToolCallIdReminter(reservedIds: Iterable<string>): (rawId: string) => string {
|
|
19
|
+
const occupied = new Set(reservedIds);
|
|
20
|
+
return rawId => {
|
|
21
|
+
if (!occupied.has(rawId)) {
|
|
22
|
+
occupied.add(rawId);
|
|
23
|
+
return rawId;
|
|
24
|
+
}
|
|
25
|
+
// A non-conforming source is sanitized, never dropped: the wire still needs an id, and the
|
|
26
|
+
// occupied check below covers a sanitized form that now equals some other call's id.
|
|
27
|
+
const base = isConformingToolCallId(rawId) ? rawId : rawId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
28
|
+
for (let n = 2; ; n++) {
|
|
29
|
+
// Hyphen, not underscore: an id that extends another id as `<earlier>_<digits>` is parsed by
|
|
30
|
+
// at least one client as a batch sub-call of `<earlier>`, which pairs the second call's
|
|
31
|
+
// result to the first call. A `-<n>` suffix is in the same id family without that reading.
|
|
32
|
+
const suffix = `-${n}`;
|
|
33
|
+
const candidate = base.slice(0, Math.max(1, MAX_TOOL_CALL_ID_LENGTH - suffix.length)) + suffix;
|
|
34
|
+
if (!occupied.has(candidate)) {
|
|
35
|
+
occupied.add(candidate);
|
|
36
|
+
return candidate;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Tool-call ids the client's own history has already fixed: every assistant tool call it kept, plus
|
|
44
|
+
* every tool result that answered one. A response repeating any of them is the collision this
|
|
45
|
+
* module exists for.
|
|
46
|
+
*
|
|
47
|
+
* Read from the client's history rather than from earlier responses because the client is the
|
|
48
|
+
* authority on uniqueness here: it is the side that drops duplicates, so the proxy cannot observe
|
|
49
|
+
* the ids it discarded — a dropped id only ever exists as the absence it caused. A Messages client
|
|
50
|
+
* sends the whole conversation on every turn, which is why one turn's history is a complete picture
|
|
51
|
+
* of the ids that may not be reused.
|
|
52
|
+
*/
|
|
53
|
+
export function reservedToolCallIdsFromHistory(messages: readonly OcxMessage[]): Set<string> {
|
|
54
|
+
const ids = new Set<string>();
|
|
55
|
+
for (const message of messages) {
|
|
56
|
+
if (message.role === "assistant") {
|
|
57
|
+
for (const part of message.content) {
|
|
58
|
+
if (part.type === "toolCall") ids.add(part.id);
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (message.role === "toolResult") ids.add(message.toolCallId);
|
|
63
|
+
}
|
|
64
|
+
return ids;
|
|
65
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isDeepseekArtifactTarget, relaxDeepseekArtifactSchema } from "./deepseek-artifact-schema";
|
|
1
2
|
import { isNativeOpenAIChatTarget } from "./wire";
|
|
2
3
|
import { createOpenAIChatToolNameRegistry, type OpenAIChatToolNameRegistry } from "./tool-name-registry";
|
|
3
4
|
import { isXaiSchemaTarget, lookupLocalJsonPointer, normalizeXaiToolParameters } from "../xai-tool-schema";
|
|
@@ -548,7 +549,10 @@ export function toolsToChatFormat(
|
|
|
548
549
|
: moonshotTarget
|
|
549
550
|
? normalizeMoonshotToolParameters(t.parameters, moonshotInlineByteBudget)
|
|
550
551
|
: ensureRootObjectType(t.parameters);
|
|
551
|
-
const
|
|
552
|
+
const deepseekArtifact = isDeepseekArtifactTarget(provider, t.name, t.namespace);
|
|
553
|
+
const parameters = stripUnicodePropertyPatterns(stripResponsesOnlyEncryptedMarker(
|
|
554
|
+
deepseekArtifact ? relaxDeepseekArtifactSchema(normalized) : normalized,
|
|
555
|
+
));
|
|
552
556
|
|
|
553
557
|
if (parameters === undefined) return [];
|
|
554
558
|
return [{
|
|
@@ -557,7 +561,7 @@ export function toolsToChatFormat(
|
|
|
557
561
|
name: registry.alias(t),
|
|
558
562
|
...(t.description ? { description: t.description } : {}),
|
|
559
563
|
parameters,
|
|
560
|
-
...(t.strict !== undefined ? { strict: t.strict } : {}),
|
|
564
|
+
...(t.strict !== undefined && !deepseekArtifact ? { strict: t.strict } : {}),
|
|
561
565
|
},
|
|
562
566
|
}];
|
|
563
567
|
});
|
|
@@ -340,7 +340,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
// Held markup is released only now, as one batch per response: the doubled-input repair needs every call.
|
|
343
|
-
const references = reconcileStructuredToolCalls(calls.map(call => ({ wireName: call.name, restoredName: toolNames.restore(call.name), argumentsText: call.args, freeformTool: freeformTools.get(call.name) })), toolCallContent.current());
|
|
343
|
+
const references = reconcileStructuredToolCalls(calls.map(call => ({ wireName: call.name, restoredName: toolNames.restore(call.name), argumentsText: call.args, freeformTool: freeformTools.get(call.name) })), toolCallContent.current(), !toolCallContent.releasedAnswerText && /(?:^|[/-])mimo-v2(?:\.|$)/i.test(lastRequestedModelId ?? ""));
|
|
344
344
|
calls.forEach((call, index) => { call.args = references[index]!.argumentsText; });
|
|
345
345
|
yield* toolCallContent.drain(references);
|
|
346
346
|
for (const call of calls) {
|
|
@@ -803,7 +803,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
803
803
|
events.push({ type: "tool_call_start", id, name: toolNames.restore(name) }, delta, { type: "tool_call_end" });
|
|
804
804
|
}
|
|
805
805
|
}
|
|
806
|
-
const references = reconcileStructuredToolCalls(structuredCalls, answerText);
|
|
806
|
+
const references = reconcileStructuredToolCalls(structuredCalls, answerText, /(?:^|[/-])mimo-v2(?:\.|$)/i.test(lastRequestedModelId ?? ""));
|
|
807
807
|
structuredCalls.forEach((call, index) => { call.delta.arguments = references[index]!.argumentsText; });
|
|
808
808
|
reconcileSerializedToolCallEvents(events, contentStart, contentEnd, references, budget);
|
|
809
809
|
const stopReason = stopReasonFor(choice.finish_reason);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isPlainObject } from "./internal";
|
|
2
|
+
|
|
3
|
+
export class MuseToolChoiceCompatibilityError extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super("This tool_choice cannot be preserved for Meta Responses; use auto or none.");
|
|
6
|
+
this.name = "MuseToolChoiceCompatibilityError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function selectorKind(choice: unknown): "auto" | "none" {
|
|
11
|
+
if (choice === undefined || choice === "auto") return "auto";
|
|
12
|
+
if (choice === "none") return "none";
|
|
13
|
+
throw new MuseToolChoiceCompatibilityError();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizeMuseToolChoice(body: unknown, originalChoice: unknown): unknown {
|
|
17
|
+
const originalKind = selectorKind(originalChoice);
|
|
18
|
+
const effectiveKind = selectorKind(isPlainObject(body) ? body.tool_choice : undefined);
|
|
19
|
+
if (originalKind !== "none" && effectiveKind !== "none") return body;
|
|
20
|
+
if (!isPlainObject(body)) return body;
|
|
21
|
+
|
|
22
|
+
const input = Array.isArray(body.input)
|
|
23
|
+
? body.input.filter(item => !isPlainObject(item) || item.type !== "additional_tools")
|
|
24
|
+
: body.input;
|
|
25
|
+
const {
|
|
26
|
+
tool_choice: _toolChoice,
|
|
27
|
+
parallel_tool_calls: _parallelToolCalls,
|
|
28
|
+
...rest
|
|
29
|
+
} = body;
|
|
30
|
+
return { ...rest, tools: [], ...(input !== body.input ? { input } : {}) };
|
|
31
|
+
}
|
|
@@ -44,6 +44,7 @@ import { applyTierDecisionToResponsesBody, normalizeCanonicalForwardContinuation
|
|
|
44
44
|
import { normalizeImageGenClientTools, preferConfiguredHostedTools } from "./image-gen";
|
|
45
45
|
import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchFields } from "./web-search";
|
|
46
46
|
import { observeOutbound } from "../../usage/cache-diagnostic";
|
|
47
|
+
import { normalizeMuseToolChoice } from "./muse-tool-choice";
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
50
|
* Identifies DeepSeek's strict Responses replay contract: tool-bearing continuations need
|
|
@@ -411,8 +412,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
411
412
|
outBody = rewritten.body;
|
|
412
413
|
convertedRoutedNamespaceToolAliases = rewritten.aliases;
|
|
413
414
|
// Preserve xAI's cached-only fail-closed semantics and image-search mapping before the
|
|
414
|
-
// generic capability fallback removes the private OpenAI fields.
|
|
415
|
-
|
|
415
|
+
// generic capability fallback removes the private OpenAI fields. Grok relayed through
|
|
416
|
+
// OpenCode Go speaks the same dialect, which the final model id and URL identify.
|
|
417
|
+
outBody = normalizeXaiResponsesWebSearch(outBody, provider, { modelId: parsed.modelId, responseUrl: url });
|
|
416
418
|
outBody = injectXaiResponsesXSearch(outBody, provider, parsed._replayPrefixLen);
|
|
417
419
|
// xAI and explicitly classified compatible gateways reject these OpenAI web_search
|
|
418
420
|
// extensions. Keep them for OpenAI API-key traffic and unclassified gateways.
|
|
@@ -494,7 +496,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
494
496
|
parsed.modelId,
|
|
495
497
|
);
|
|
496
498
|
// Normalize the wire model before deriving model-dependent transport metadata.
|
|
497
|
-
|
|
499
|
+
let finalBody =
|
|
498
500
|
provider.modelSuffixBracketStrip
|
|
499
501
|
&& unnormalizedBody !== null
|
|
500
502
|
&& typeof unnormalizedBody === "object"
|
|
@@ -502,6 +504,10 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
502
504
|
&& typeof (unnormalizedBody as { model?: unknown }).model === "string"
|
|
503
505
|
? { ...(unnormalizedBody as Record<string, unknown>), model: stripBracketedModelSuffix((unnormalizedBody as { model: string }).model) }
|
|
504
506
|
: unnormalizedBody;
|
|
507
|
+
if (isMetaAiResponsesDestination(url)) {
|
|
508
|
+
const originalChoice = isPlainObject(parsed._rawBody) ? parsed._rawBody.tool_choice : undefined;
|
|
509
|
+
finalBody = normalizeMuseToolChoice(finalBody, originalChoice);
|
|
510
|
+
}
|
|
505
511
|
if (isCanonicalOpenAiForwardProvider(provider)) {
|
|
506
512
|
const routingHeaders = new Headers(headers);
|
|
507
513
|
applyCodexRoutingHint(routingHeaders, finalBody);
|
|
@@ -2,22 +2,31 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
2
2
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* True only for the exact OpenCode Go Responses resource. Lookalike hosts, other Zen paths,
|
|
7
|
+
* credentials, queries, fragments and unparsable URLs are all excluded, so a custom provider
|
|
8
|
+
* that merely resembles Go never inherits a Go-specific body rewrite.
|
|
9
|
+
*/
|
|
10
|
+
export function isOpenCodeGoResponsesUrl(responseUrl: string): boolean {
|
|
11
|
+
let destination: URL;
|
|
12
|
+
try {
|
|
13
|
+
destination = new URL(responseUrl);
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return destination.origin === "https://opencode.ai"
|
|
18
|
+
&& destination.pathname === "/zen/go/v1/responses"
|
|
19
|
+
&& !destination.username && !destination.password
|
|
20
|
+
&& !destination.href.includes("?") && !destination.href.includes("#");
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
/** Console Go accepts public tools but rejects the private additional_tools input wrapper. */
|
|
6
24
|
export function normalizeOpenCodeGoAdditionalTools(
|
|
7
25
|
body: unknown,
|
|
8
26
|
responseUrl: string,
|
|
9
27
|
replayPrefixLength = 0,
|
|
10
28
|
): unknown {
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
destination = new URL(responseUrl);
|
|
14
|
-
} catch {
|
|
15
|
-
return body;
|
|
16
|
-
}
|
|
17
|
-
if (destination.origin !== "https://opencode.ai"
|
|
18
|
-
|| destination.pathname !== "/zen/go/v1/responses"
|
|
19
|
-
|| destination.username || destination.password
|
|
20
|
-
|| destination.href.includes("?") || destination.href.includes("#")) return body;
|
|
29
|
+
if (!isOpenCodeGoResponsesUrl(responseUrl)) return body;
|
|
21
30
|
if (!isRecord(body) || !Array.isArray(body.input)) return body;
|
|
22
31
|
// Do not replace a malformed top-level catalog with a partial promoted one.
|
|
23
32
|
if (body.tools !== undefined && !Array.isArray(body.tools)) return body;
|