@bitkyc08/opencodex 2.87.0 → 2.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/bin/ocx.mjs +15 -4
- package/gui/dist/assets/App-BnRSujOq.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-DCd-A1az.js → Tray-7z1COnmz.js} +1 -1
- package/gui/dist/assets/index-uchD-ngf.js +94 -0
- package/gui/dist/assets/{quota-summary-ChvmKGOk.js → quota-summary-BY_10IEz.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +86 -48
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +52 -30
- package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/passthrough.ts +17 -12
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/registry.ts +1 -1
- package/src/adapters/upstream-http-error.ts +3 -2
- package/src/adapters/zed.ts +7 -4
- package/src/bridge/internal.ts +17 -3
- package/src/bridge/response-json.ts +4 -4
- package/src/bridge/sse.ts +17 -10
- package/src/claude/inbound-content-options.ts +66 -0
- package/src/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +18 -5
- package/src/cli/account-api.ts +13 -1
- package/src/cli/account-extended.ts +17 -5
- package/src/cli/account-key-quota.ts +4 -1
- package/src/cli/account.ts +1 -1
- package/src/cli/capabilities-agents-routing.ts +45 -8
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-provider-models.ts +5 -4
- package/src/cli/combo-result.ts +8 -2
- package/src/cli/combo.ts +17 -0
- package/src/cli/cross-home-owner.ts +16 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/loopback-family.ts +31 -0
- package/src/cli/message-command.ts +4 -1
- package/src/cli/message-remote-args.ts +69 -0
- package/src/cli/message-remote-command.ts +77 -0
- package/src/cli/provider-runtime.ts +69 -1
- package/src/cli/registry.ts +3 -3
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +30 -3
- package/src/codex/auth-api/account-list.ts +3 -0
- package/src/codex/auth-api/main-account-probe.ts +3 -2
- package/src/codex/auth-context.ts +9 -4
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/auto-review.ts +2 -1
- package/src/codex/catalog/build-entries.ts +17 -7
- package/src/codex/catalog/derive-entry.ts +3 -1
- package/src/codex/catalog/metadata.ts +2 -0
- package/src/codex/catalog/model-visibility.ts +0 -2
- package/src/codex/catalog/ownership.ts +71 -0
- package/src/codex/catalog/parsing.ts +12 -3
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/catalog/routed-gather.ts +3 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- package/src/codex/convergence.ts +2 -1
- package/src/codex/low-quota-desktop-notifier.ts +125 -0
- package/src/codex/low-quota-protection.ts +57 -31
- package/src/codex/main-account-cache.ts +77 -3
- package/src/codex/main-account.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- package/src/codex/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- package/src/combos/failover.ts +2 -1
- package/src/combos/jev-model-backend.ts +30 -7
- package/src/combos/jev-model-request.ts +20 -0
- package/src/combos/jev-quota-activation.ts +153 -0
- package/src/combos/jev-quota-config.ts +62 -0
- package/src/combos/jev-quota-route.ts +26 -0
- package/src/combos/jev-quota.ts +88 -0
- package/src/combos/jev.ts +25 -12
- package/src/combos/types.ts +27 -0
- package/src/compatibility/index.ts +3 -0
- package/src/compatibility/openai-responses.ts +8 -1
- package/src/compatibility/opengateway-responses.ts +61 -0
- package/src/config/account-auto-pause-revisions.ts +136 -0
- package/src/config/diagnostics.ts +13 -0
- package/src/config/live-reconcile.ts +9 -0
- package/src/config/opencode-zen-upgrade.ts +9 -8
- package/src/config/persist-unlocked.ts +10 -1
- package/src/config/removed-opencode-free.ts +135 -0
- package/src/config/schema/account-auto-pause.ts +14 -0
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +7 -0
- package/src/config.ts +10 -3
- package/src/generated/compatibility-version.json +411 -175
- package/src/images/loop.ts +101 -27
- package/src/lib/account-auto-pause-response.ts +16 -0
- package/src/lib/bun-runtime.ts +18 -1
- package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/request-failure-model.ts +2 -0
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/link/ssh-argv.ts +3 -0
- package/src/messaging/envelope.ts +6 -3
- package/src/messaging/remote-auth.ts +83 -0
- package/src/messaging/remote-bridge.ts +270 -0
- package/src/messaging/remote-contract.ts +108 -0
- package/src/messaging/remote-enrollment-recovery.ts +59 -0
- package/src/messaging/remote-enrollment.ts +244 -0
- package/src/messaging/remote-envelope.ts +40 -0
- package/src/messaging/remote-files.ts +117 -0
- package/src/messaging/remote-owner.ts +169 -0
- package/src/messaging/remote-ports.ts +56 -0
- package/src/messaging/remote-process.ts +131 -0
- package/src/messaging/remote-replies.ts +98 -0
- package/src/messaging/remote-rpc-admission.ts +32 -0
- package/src/messaging/remote-send.ts +72 -0
- package/src/messaging/remote-store.ts +111 -0
- package/src/messaging/remote-tunnels.ts +65 -0
- package/src/messaging/rpc.ts +13 -1
- package/src/oauth/account-auto-pause-policy.ts +66 -0
- package/src/oauth/account-auto-pause.ts +171 -0
- package/src/oauth/account-quota-rank.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +6 -0
- package/src/oauth/anthropic-model-family.ts +8 -0
- package/src/oauth/anthropic-model-quota.ts +2 -6
- package/src/oauth/api-key-pool-preserve.ts +37 -0
- package/src/oauth/generic-account-failover.ts +7 -4
- package/src/oauth/index.ts +5 -35
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/protocols/encoders/adapter-events.ts +5 -5
- package/src/protocols/encoders/messages.ts +2 -2
- package/src/providers/api-key-pause.ts +32 -0
- package/src/providers/api-key-resolve.ts +10 -4
- package/src/providers/api-key-selection.ts +6 -1
- package/src/providers/api-keys.ts +8 -4
- package/src/providers/claude-cli-usage.ts +5 -0
- package/src/providers/key-failover.ts +11 -11
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +12 -73
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +2 -1
- package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
- package/src/providers/quota/anthropic-decision-windows.ts +47 -0
- package/src/providers/quota/antigravity.ts +39 -6
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota-decision-publication.ts +77 -0
- package/src/providers/quota-decision-snapshot.ts +145 -0
- package/src/providers/quota-routing-cache.ts +3 -0
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +3 -2
- package/src/providers/registry/entries-extended.ts +8 -52
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/responses/parser.ts +18 -10
- package/src/responses/reasoning-replay-cache.ts +50 -4
- package/src/responses/web-search-replay.ts +138 -0
- package/src/routing/analytics.ts +7 -15
- package/src/routing/compatibility/behavior.ts +4 -1
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +2 -1
- package/src/server/chat-native.ts +28 -13
- package/src/server/claude-messages.ts +4 -2
- package/src/server/index/live-sideband.ts +3 -2
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +11 -5
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +7 -7
- package/src/server/lifecycle.ts +13 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +10 -1
- package/src/server/management/provider-completion-test.ts +289 -0
- package/src/server/management/provider-routes.ts +9 -1
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/messages-native.ts +34 -7
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +7 -2
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +34 -18
- package/src/server/responses/adapter-delivery.ts +53 -6
- package/src/server/responses/adapter-dispatch.ts +104 -29
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +6 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +8 -5
- package/src/server/responses/compaction-recovery.ts +2 -0
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-codex-account.ts +9 -3
- package/src/server/responses/core-combo.ts +34 -7
- package/src/server/responses/core-lifetime.ts +3 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +5 -1
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/native-steering-log.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +30 -33
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +1 -0
- package/src/server/responses/request-transport.ts +32 -3
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +59 -10
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/startup-action-control.ts +2 -2
- package/src/server/startup-health-cache.ts +2 -2
- package/src/server/windows-tray-control.ts +2 -2
- package/src/server/ws-bridge.ts +2 -0
- package/src/service/definition.ts +9 -1
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/opencode-zen.ts +9 -3
- package/src/types/provider.ts +9 -5
- package/src/types/request.ts +30 -4
- package/src/types/wire.ts +4 -2
- package/src/types.ts +2 -0
- package/src/usage/cost.ts +17 -1
- package/src/usage/expected-prices.ts +3 -1
- package/src/usage/jev-stats.ts +4 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +16 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/web-search/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +212 -73
- package/src/web-search/progress-stream.ts +14 -1
- package/src/web-search/run-turn-loop.ts +9 -3
- package/src/web-search/sidecar-search-replay.ts +96 -0
- package/gui/dist/assets/App-ZfYbLHkB.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/index-CfYDMj34.js +0 -94
package/src/adapters/google.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
observeAntigravityReplay,
|
|
37
37
|
} from "./google-antigravity-replay";
|
|
38
38
|
import { canonicalAntigravityUsageModel, resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
|
|
39
|
-
import {
|
|
39
|
+
import { selectVertexEndpoint, vertexInferenceUrl, vertexReplayModelKey } from "./google-vertex-endpoint";
|
|
40
40
|
import { forgetThoughtSignatureForReplay, lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
|
|
41
41
|
import {
|
|
42
42
|
isTranslatorBudgetExceededError,
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
type TranslatorBudget,
|
|
45
45
|
} from "../lib/translator-budget";
|
|
46
46
|
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
47
|
+
import { geminiProgressLead, trimsGeminiProgressHistory } from "./google-progress-history";
|
|
47
48
|
import { configuredReasoningEfforts, mapReasoningEffort } from "../reasoning-effort";
|
|
48
49
|
|
|
49
50
|
// Google-family models (Gemini/Vertex/Antigravity) tend to emit long running commentary between
|
|
@@ -332,13 +333,20 @@ function geminiToolResultParts(
|
|
|
332
333
|
msg: OcxToolResultMessage,
|
|
333
334
|
wireName: string,
|
|
334
335
|
wireCallId: string,
|
|
336
|
+
nestToolResultImages: boolean,
|
|
335
337
|
): unknown[] {
|
|
336
338
|
const functionResponse: Record<string, unknown> = {
|
|
337
339
|
name: wireName,
|
|
338
340
|
response: { result: geminiToolResultText(msg.content) },
|
|
339
341
|
id: wireCallId,
|
|
340
342
|
};
|
|
341
|
-
|
|
343
|
+
const images = toolResultImageParts(msg.content);
|
|
344
|
+
// Vertex Argon rejects image siblings beside a functionResponse as an unsupported model tail.
|
|
345
|
+
if (nestToolResultImages && images.length > 0) {
|
|
346
|
+
functionResponse.parts = images;
|
|
347
|
+
return [{ functionResponse }];
|
|
348
|
+
}
|
|
349
|
+
return [{ functionResponse }, ...images];
|
|
342
350
|
}
|
|
343
351
|
|
|
344
352
|
function geminiMissingToolResultPart(wireName: string, wireCallId: string): unknown {
|
|
@@ -369,6 +377,7 @@ function messagesToGeminiFormat(
|
|
|
369
377
|
identityModelId: string,
|
|
370
378
|
stripRejectedClaudeSdkParagraph = false,
|
|
371
379
|
isCloudCodeAssist = false,
|
|
380
|
+
nestToolResultImages = false,
|
|
372
381
|
): { systemInstruction?: unknown; contents: unknown[]; replayedCallIds: string[] } {
|
|
373
382
|
// Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
|
|
374
383
|
// never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
|
|
@@ -466,11 +475,26 @@ function messagesToGeminiFormat(
|
|
|
466
475
|
const aMsg = msg as OcxAssistantMessage;
|
|
467
476
|
const parts: unknown[] = [];
|
|
468
477
|
const toolCalls: Array<{ wireCallId: string; wireName: string }> = [];
|
|
478
|
+
// Progress text before a Gemini tool call replays as its lead sentence, so planning the
|
|
479
|
+
// model leaked into visible text is not copied forward (google-progress-history.ts).
|
|
480
|
+
const trimProgress = trimsGeminiProgressHistory(identityModelId)
|
|
481
|
+
&& aMsg.phase !== "final_answer"
|
|
482
|
+
&& aMsg.content.some(part => part.type === "toolCall");
|
|
483
|
+
let progressLeadKept = false;
|
|
484
|
+
let afterToolCall = false;
|
|
469
485
|
for (const p of aMsg.content) {
|
|
470
486
|
if (p.type === "text") {
|
|
471
|
-
|
|
487
|
+
let text: unknown = (p as OcxTextContent).text;
|
|
488
|
+
if (trimProgress && !afterToolCall) {
|
|
489
|
+
if (progressLeadKept || typeof text !== "string") continue;
|
|
490
|
+
text = geminiProgressLead(text);
|
|
491
|
+
if (text === undefined) continue;
|
|
492
|
+
progressLeadKept = true;
|
|
493
|
+
}
|
|
494
|
+
const textPart = geminiTextPart(text);
|
|
472
495
|
if (textPart) parts.push(textPart);
|
|
473
496
|
} else if (p.type === "toolCall") {
|
|
497
|
+
afterToolCall = true;
|
|
474
498
|
const tc = p as OcxToolCall;
|
|
475
499
|
// Preserve the thought signature on the function-call part so Antigravity/Gemini-3
|
|
476
500
|
// reasoning continuity survives history-driven (stateless) turns, not just same-process
|
|
@@ -535,13 +559,17 @@ function messagesToGeminiFormat(
|
|
|
535
559
|
const responseParts: unknown[] = [];
|
|
536
560
|
for (const call of toolCalls) {
|
|
537
561
|
const result = resultsById.get(call.wireCallId);
|
|
538
|
-
if (result) responseParts.push(...geminiToolResultParts(result, call.wireName, call.wireCallId));
|
|
562
|
+
if (result) responseParts.push(...geminiToolResultParts(result, call.wireName, call.wireCallId, nestToolResultImages));
|
|
539
563
|
else responseParts.push(geminiMissingToolResultPart(call.wireName, call.wireCallId));
|
|
540
564
|
}
|
|
541
|
-
|
|
542
|
-
responseParts.push(...geminiOrphanToolResultParts(orphan));
|
|
543
|
-
}
|
|
565
|
+
const orphanParts = orphanResults.flatMap(geminiOrphanToolResultParts);
|
|
544
566
|
contents.push({ role: "user", parts: responseParts });
|
|
567
|
+
// Orphan images cannot share Argon's response batch or claim another call's ownership.
|
|
568
|
+
if (nestToolResultImages && orphanParts.some(part => typeof part === "object" && part !== null && "inline_data" in part)) {
|
|
569
|
+
contents.push({ role: "user", parts: orphanParts });
|
|
570
|
+
} else {
|
|
571
|
+
responseParts.push(...orphanParts);
|
|
572
|
+
}
|
|
545
573
|
i = j - 1;
|
|
546
574
|
}
|
|
547
575
|
break;
|
|
@@ -1010,6 +1038,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1010
1038
|
identityModelId,
|
|
1011
1039
|
stripRejectedClaudeSdkParagraph,
|
|
1012
1040
|
isCloudCodeAssist,
|
|
1041
|
+
provider.googleMode === "vertex" && parsed.modelId === "gemini-4-argon",
|
|
1013
1042
|
);
|
|
1014
1043
|
lastInjectedCallIds = [...replayedCallIds];
|
|
1015
1044
|
lastReasoningReplayScope = parsed._reasoningReplayScope;
|
|
@@ -1176,9 +1205,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1176
1205
|
const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
|
|
1177
1206
|
reportToolSchemaLoss(compiled.toolSchemaLossReport);
|
|
1178
1207
|
restoreGoogleToolName = compiled.restoreToolName;
|
|
1179
|
-
const
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1208
|
+
const apiKey = resolveVertexApiKey(provider.apiKey);
|
|
1209
|
+
const vertexEndpoint = selectVertexEndpoint(provider, Boolean(apiKey));
|
|
1210
|
+
const url = vertexInferenceUrl(vertexEndpoint, parsed.modelId, method, streamParam);
|
|
1211
|
+
vertexReplayModel = vertexReplayModelKey(vertexEndpoint, parsed.modelId);
|
|
1182
1212
|
vertexReplaySession = vertexReplaySessionId(parsed);
|
|
1183
1213
|
// Compile names before replay so the cache matches the exact provider-visible
|
|
1184
1214
|
// functionCall identity. This is the same bounded TTL/LRU store used by CCA, with the
|
|
@@ -1194,27 +1224,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1194
1224
|
(compiled.body as { contents: unknown[] }).contents,
|
|
1195
1225
|
);
|
|
1196
1226
|
}
|
|
1197
|
-
//
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1227
|
+
// Endpoint, location and model path were validated above, before any credential is attached.
|
|
1228
|
+
if (apiKey) headers["x-goog-api-key"] = apiKey;
|
|
1229
|
+
else {
|
|
1230
|
+
// Resolve each marker per request; never share a process-global active account.
|
|
1231
|
+
const markerAccount = gcpCredentialMarkerAccount(provider.apiKey);
|
|
1232
|
+
headers["Authorization"] = `Bearer ${await getVertexAccessToken({ markerAccount })}`;
|
|
1203
1233
|
}
|
|
1204
|
-
const project = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
|
|
1205
|
-
if (!project) throw new Error("Vertex AI requires a project id (provider.project or GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT).");
|
|
1206
|
-
const location = provider.location || process.env.GOOGLE_CLOUD_LOCATION;
|
|
1207
|
-
if (!location) throw new Error("Vertex AI requires a location (provider.location or GOOGLE_CLOUD_LOCATION).");
|
|
1208
|
-
const locationError = googleVertexLocationConfigError(location);
|
|
1209
|
-
if (locationError) throw new Error(locationError);
|
|
1210
|
-
const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
|
|
1211
|
-
const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
|
|
1212
|
-
// The marker account travels per-request as an argument (never process-global state): a
|
|
1213
|
-
// module-level "active marker" written at routing time would race across concurrent
|
|
1214
|
-
// requests for different providers and could resolve one call with another's credential.
|
|
1215
|
-
const markerAccount = gcpCredentialMarkerAccount(provider.apiKey);
|
|
1216
|
-
const token = await getVertexAccessToken({ markerAccount });
|
|
1217
|
-
headers["Authorization"] = `Bearer ${token}`;
|
|
1218
1234
|
return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
|
|
1219
1235
|
}
|
|
1220
1236
|
|
|
@@ -1238,6 +1254,12 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1238
1254
|
const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
|
|
1239
1255
|
reportToolSchemaLoss(compiled.toolSchemaLossReport);
|
|
1240
1256
|
restoreGoogleToolName = compiled.restoreToolName;
|
|
1257
|
+
// A functionCall issued by another model (a combo hop, #7298) has no Gemini signature, and
|
|
1258
|
+
// Gemini 3 refuses the turn without one. The same Gemini-only sentinel CCA and Vertex
|
|
1259
|
+
// apply; a real signature carried by history always wins.
|
|
1260
|
+
if (Array.isArray((compiled.body as { contents?: unknown[] }).contents)) {
|
|
1261
|
+
applyAntigravityThoughtSignatureFallback(routedModelId, (compiled.body as { contents: unknown[] }).contents);
|
|
1262
|
+
}
|
|
1241
1263
|
return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
|
|
1242
1264
|
},
|
|
1243
1265
|
|
|
@@ -114,3 +114,30 @@ export function renderCompactionSearchHistoryItems(items: readonly unknown[], re
|
|
|
114
114
|
if (omissionIndex >= 0) out[omissionIndex] = omissionNote(omitted);
|
|
115
115
|
return out;
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
function isHostedSearchTool(tool: unknown): boolean {
|
|
119
|
+
return isPlainObject(tool) && typeof tool.type === "string" && tool.type.startsWith("web_search");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* True when the outbound body declares a hosted search tool, either top-level or inside a Lite
|
|
124
|
+
* `additional_tools` input wrapper. Read on the final body, after unsupported hosted tools were
|
|
125
|
+
* stripped, so a declaration the destination never sees does not count.
|
|
126
|
+
*/
|
|
127
|
+
export function declaresHostedWebSearch(body: Record<string, unknown>): boolean {
|
|
128
|
+
if (Array.isArray(body.tools) && body.tools.some(isHostedSearchTool)) return true;
|
|
129
|
+
return Array.isArray(body.input) && body.input.some(item => isPlainObject(item)
|
|
130
|
+
&& item.type === "additional_tools" && Array.isArray(item.tools) && item.tools.some(isHostedSearchTool));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Project unreferenced hosted `web_search_call` history into reference notes. A same-input
|
|
135
|
+
* `item_reference` to a hosted cell keeps that cell structural. Returns the original body when
|
|
136
|
+
* there is nothing to project.
|
|
137
|
+
*/
|
|
138
|
+
export function projectHostedSearchHistory(body: Record<string, unknown>): Record<string, unknown> {
|
|
139
|
+
if (!Array.isArray(body.input) || !body.input.some(item => isPlainObject(item) && item.type === "web_search_call")) return body;
|
|
140
|
+
const referencedIds = new Set(body.input.flatMap(item => isPlainObject(item)
|
|
141
|
+
&& item.type === "item_reference" && typeof item.id === "string" ? [item.id] : []));
|
|
142
|
+
return { ...body, input: renderCompactionSearchHistoryItems(body.input, referencedIds) };
|
|
143
|
+
}
|
|
@@ -53,7 +53,7 @@ import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchField
|
|
|
53
53
|
import { observeOutbound } from "../../usage/cache-diagnostic";
|
|
54
54
|
|
|
55
55
|
import { normalizeMuseToolChoice } from "./muse-tool-choice";
|
|
56
|
-
import { renderCompactionSearchHistoryItems } from "./compaction-search-history";
|
|
56
|
+
import { declaresHostedWebSearch, projectHostedSearchHistory, renderCompactionSearchHistoryItems } from "./compaction-search-history";
|
|
57
57
|
import { stripKimiEmptyAssistantMessages } from "./empty-assistant";
|
|
58
58
|
|
|
59
59
|
/**
|
|
@@ -135,7 +135,8 @@ function usageFromResponsesPayload(payload: unknown): OcxUsage | undefined {
|
|
|
135
135
|
const knownKeys = new Set(["input_tokens", "output_tokens", "total_tokens", "input_tokens_details", "output_tokens_details"]);
|
|
136
136
|
const hasExtras = Object.keys(usage).some(key => !knownKeys.has(key))
|
|
137
137
|
|| (isPlainObject(usage.input_tokens_details)
|
|
138
|
-
&& Object.keys(usage.input_tokens_details).some(key => key !== "cached_tokens" && key !== "cache_write_tokens"
|
|
138
|
+
&& Object.keys(usage.input_tokens_details).some(key => key !== "cached_tokens" && key !== "cache_write_tokens"
|
|
139
|
+
&& key !== "cache_write_1h_tokens"))
|
|
139
140
|
|| (isPlainObject(usage.output_tokens_details)
|
|
140
141
|
&& Object.keys(usage.output_tokens_details).some(key => key !== "reasoning_tokens"));
|
|
141
142
|
if (inputTokens === 0 && outputTokens === 0 && !hasExtras) return undefined;
|
|
@@ -147,6 +148,8 @@ function usageFromResponsesPayload(payload: unknown): OcxUsage | undefined {
|
|
|
147
148
|
...(typeof usage.total_tokens === "number" ? { totalTokens: usage.total_tokens } : {}),
|
|
148
149
|
...(typeof inputDetails?.cached_tokens === "number" ? { cachedInputTokens: inputDetails.cached_tokens } : {}),
|
|
149
150
|
...(typeof inputDetails?.cache_write_tokens === "number" ? { cacheCreationInputTokens: inputDetails.cache_write_tokens } : {}),
|
|
151
|
+
...(typeof inputDetails?.cache_write_1h_tokens === "number"
|
|
152
|
+
? { cacheCreation1hInputTokens: inputDetails.cache_write_1h_tokens } : {}),
|
|
150
153
|
...(typeof outputDetails?.reasoning_tokens === "number" ? { reasoningOutputTokens: outputDetails.reasoning_tokens } : {}),
|
|
151
154
|
...(hasExtras ? { rawUsage: { ...usage } } : {}),
|
|
152
155
|
};
|
|
@@ -492,16 +495,18 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
492
495
|
// here, on the serialized body, not on the parsed selector. One place covers both the
|
|
493
496
|
// HTTP and the WebSocket outbound, because the WS path transports this same request
|
|
494
497
|
// instead of rebuilding it.
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
498
|
+
// Hosted-search history projection, after bridge restoration and tool validation. Two
|
|
499
|
+
// triggers share one bounded renderer, and tool declarations and other input stay intact:
|
|
500
|
+
// 1. The opt-in Codex Lite policy covers ordinary turns and native v2 compaction triggers
|
|
501
|
+
// (#7268). Portable compaction was already projected by the summary builder above.
|
|
502
|
+
// 2. Any request whose final tool surface declares no hosted search. Native Sol answers a
|
|
503
|
+
// replayed web_search_call without a matching declaration with a generic server_error
|
|
504
|
+
// (#7250), so the cells become untrusted reference notes instead of a doomed request.
|
|
505
|
+
if (isPlainObject(finalBody)
|
|
506
|
+
&& ((provider.projectResponsesLiteHostedSearchHistory === true && url === `${CODEX_FORWARD_BASE_URL}/responses`
|
|
507
|
+
&& !parsed._portableCompaction && codexResponsesLiteSelected(headers, finalBody))
|
|
508
|
+
|| !declaresHostedWebSearch(finalBody))) {
|
|
509
|
+
finalBody = projectHostedSearchHistory(finalBody);
|
|
505
510
|
}
|
|
506
511
|
observeOutbound(parsed._rawBody, finalBody, headers);
|
|
507
512
|
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
@@ -163,7 +163,74 @@ export function sanitizeReasoningInputContent(
|
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
if (blankedPlaintext) opts?.onPlaintextReasoningBlanked?.();
|
|
166
|
-
|
|
166
|
+
// Runs after the per-item pass so an item it emptied has already been backfilled, and a
|
|
167
|
+
// placeholder is only invented for a turn that carries no reasoning item at all.
|
|
168
|
+
const withTurnReasoning = opts?.requirePlaintextReasoning === true
|
|
169
|
+
? backfillBareToolCallTurnReasoning(input)
|
|
170
|
+
: input;
|
|
171
|
+
if (withTurnReasoning !== input) changed = true;
|
|
172
|
+
return changed ? { ...raw, input: withTurnReasoning } : body;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Assistant-authored history items: what DeepSeek merges into one assistant message. */
|
|
176
|
+
function isAssistantTurnItem(item: unknown): boolean {
|
|
177
|
+
if (!isPlainObject(item) || typeof item.type !== "string") return false;
|
|
178
|
+
if (item.type === "reasoning") return true;
|
|
179
|
+
if (item.type === "message") return item.role === "assistant";
|
|
180
|
+
return item.type.endsWith("_call");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isClientToolCallItem(item: unknown): boolean {
|
|
184
|
+
return isPlainObject(item)
|
|
185
|
+
&& (item.type === "function_call" || item.type === "custom_tool_call" || item.type === "local_shell_call");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Give every assistant tool-call turn that has NO reasoning item a minimal plaintext one (#7298).
|
|
190
|
+
*
|
|
191
|
+
* A Combo can serve the turn that issued a tool call on one model and the continuation on
|
|
192
|
+
* another. When the issuing model was a GPT turn that produced no reasoning item (or the item
|
|
193
|
+
* was dropped upstream), the strict destination sees a bare `function_call` and DeepSeek's
|
|
194
|
+
* thinking mode refuses the whole request with `The reasoning_text in the thinking mode must be
|
|
195
|
+
* passed back to the API`. The per-item backfill above cannot help, because there is no item to
|
|
196
|
+
* backfill. This is the Responses-wire twin of the Chat adapter's `reasoning_content: " "`
|
|
197
|
+
* placeholder (#5421): the same single-space text, never fabricated reasoning, inserted at the
|
|
198
|
+
* head of the turn so it precedes the call exactly where a native turn's reasoning would be.
|
|
199
|
+
*
|
|
200
|
+
* A turn is a maximal run of assistant-authored items; any user message or tool output ends it.
|
|
201
|
+
* Turns that already carry a reasoning item, and turns with no client tool call, are untouched.
|
|
202
|
+
*/
|
|
203
|
+
export function backfillBareToolCallTurnReasoning(input: unknown[]): unknown[] {
|
|
204
|
+
const bareTurnStarts = new Set<number>();
|
|
205
|
+
let turnStart = -1;
|
|
206
|
+
let turnHasReasoning = false;
|
|
207
|
+
let turnHasToolCall = false;
|
|
208
|
+
const closeTurn = (): void => {
|
|
209
|
+
if (turnStart >= 0 && turnHasToolCall && !turnHasReasoning) bareTurnStarts.add(turnStart);
|
|
210
|
+
turnStart = -1;
|
|
211
|
+
turnHasReasoning = false;
|
|
212
|
+
turnHasToolCall = false;
|
|
213
|
+
};
|
|
214
|
+
for (let index = 0; index < input.length; index++) {
|
|
215
|
+
const item = input[index];
|
|
216
|
+
if (!isAssistantTurnItem(item)) {
|
|
217
|
+
closeTurn();
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (turnStart < 0) turnStart = index;
|
|
221
|
+
if ((item as Record<string, unknown>).type === "reasoning") turnHasReasoning = true;
|
|
222
|
+
if (isClientToolCallItem(item)) turnHasToolCall = true;
|
|
223
|
+
}
|
|
224
|
+
closeTurn();
|
|
225
|
+
if (bareTurnStarts.size === 0) return input;
|
|
226
|
+
const result: unknown[] = [];
|
|
227
|
+
for (let index = 0; index < input.length; index++) {
|
|
228
|
+
if (bareTurnStarts.has(index)) {
|
|
229
|
+
result.push({ type: "reasoning", summary: [], content: [{ type: "reasoning_text", text: " " }] });
|
|
230
|
+
}
|
|
231
|
+
result.push(input[index]);
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
167
234
|
}
|
|
168
235
|
|
|
169
236
|
/**
|
package/src/adapters/registry.ts
CHANGED
|
@@ -138,7 +138,7 @@ export const ADAPTER_REGISTRY = {
|
|
|
138
138
|
zed: {
|
|
139
139
|
wire: "zed",
|
|
140
140
|
mutation: "codex-owned",
|
|
141
|
-
create: (provider: OcxProviderConfig,
|
|
141
|
+
create: (provider: OcxProviderConfig, context: AdapterFactoryContext) => createZedAdapter(provider, context.cacheRetention),
|
|
142
142
|
},
|
|
143
143
|
"mimo-free": {
|
|
144
144
|
contractParent: "openai-chat",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inheritAccountAutoPauseResponse } from "../lib/account-auto-pause-response";
|
|
1
2
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
2
3
|
import { redactSecretString } from "../lib/redact";
|
|
3
4
|
|
|
@@ -40,9 +41,9 @@ export async function normalizeUpstreamHttpErrorResponse(
|
|
|
40
41
|
const headers = new Headers(res.headers);
|
|
41
42
|
headers.delete("content-encoding");
|
|
42
43
|
headers.delete("content-length");
|
|
43
|
-
return new Response(await opts.formatMessage(payloadText), {
|
|
44
|
+
return inheritAccountAutoPauseResponse(res, new Response(await opts.formatMessage(payloadText), {
|
|
44
45
|
status: res.status,
|
|
45
46
|
statusText: res.statusText,
|
|
46
47
|
headers,
|
|
47
|
-
});
|
|
48
|
+
}));
|
|
48
49
|
}
|
package/src/adapters/zed.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base";
|
|
3
3
|
import { createAnthropicAdapter } from "./anthropic";
|
|
4
|
+
import type { AnthropicCacheRetention } from "./anthropic/cache-retention";
|
|
4
5
|
import { createGoogleAdapter } from "./google";
|
|
5
6
|
import { createOpenAIChatAdapter } from "./openai-chat";
|
|
6
7
|
import { createResponsesPassthroughAdapter } from "./openai-responses";
|
|
@@ -218,15 +219,17 @@ export function zedEventStream(
|
|
|
218
219
|
return body.pipeThrough(output);
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
function createDelegate(provider: OcxProviderConfig, zedProvider: ZedProvider): ProviderAdapter {
|
|
222
|
+
function createDelegate(provider: OcxProviderConfig, zedProvider: ZedProvider, cacheRetention?: AnthropicCacheRetention): ProviderAdapter {
|
|
222
223
|
const config = delegateProvider(provider, zedProvider);
|
|
223
|
-
|
|
224
|
+
// The delegate only borrows the api.anthropic.com body builder; the wire goes to Zed's
|
|
225
|
+
// gateway, which does not document the 1h TTL, so an omitted retention stays 5m here.
|
|
226
|
+
if (zedProvider === "anthropic") return createAnthropicAdapter(config, cacheRetention ?? "short");
|
|
224
227
|
if (zedProvider === "google") return createGoogleAdapter(config);
|
|
225
228
|
if (zedProvider === "open_ai") return createResponsesPassthroughAdapter(config);
|
|
226
229
|
return createOpenAIChatAdapter(config);
|
|
227
230
|
}
|
|
228
231
|
|
|
229
|
-
export function createZedAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
232
|
+
export function createZedAdapter(provider: OcxProviderConfig, cacheRetention?: AnthropicCacheRetention): ProviderAdapter {
|
|
230
233
|
let delegate: ZedDelegate | undefined;
|
|
231
234
|
let credentials: ZedCredentials | undefined;
|
|
232
235
|
|
|
@@ -246,7 +249,7 @@ export function createZedAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
|
246
249
|
/* Model inference fallback below; a transient catalog outage must not block passthrough. */
|
|
247
250
|
}
|
|
248
251
|
const zedProvider = providerFromCatalog(catalog, parsed.modelId);
|
|
249
|
-
const selected = createDelegate(provider, zedProvider);
|
|
252
|
+
const selected = createDelegate(provider, zedProvider, cacheRetention);
|
|
250
253
|
const safeIncoming: IncomingMeta = {
|
|
251
254
|
headers: incoming?.headers ?? new Headers(),
|
|
252
255
|
translatorBudget: incoming?.translatorBudget ?? createTranslatorBudget(),
|
package/src/bridge/internal.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AdapterEvent,
|
|
3
|
+
AnthropicServerBlocks,
|
|
3
4
|
OcxMessagePhase,
|
|
4
5
|
OcxProviderContinuationState,
|
|
5
6
|
OcxProviderOpaqueToolCallMetadata,
|
|
@@ -22,6 +23,11 @@ export function uuid(): string {
|
|
|
22
23
|
return crypto.randomUUID().replace(/-/g, "");
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/** The client-visible id of a hosted search cell: the producer's `ws_` id when well-formed, else a fresh one. */
|
|
27
|
+
export function webSearchCellItemId(candidate: string | undefined): string {
|
|
28
|
+
return typeof candidate === "string" && /^ws_[A-Za-z0-9_-]{1,128}$/.test(candidate) ? candidate : `ws_${uuid()}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
/** Test-only: bound the abandoned-owned-budget watchdog delay (null restores). */
|
|
26
32
|
export let ownedBudgetAbandonedMs = 10 * 60 * 1000;
|
|
27
33
|
const OWNED_BUDGET_ABANDONED_DEFAULT_MS = ownedBudgetAbandonedMs;
|
|
@@ -63,7 +69,7 @@ export function responsesUsage(usage: OcxUsage | undefined): Record<string, unkn
|
|
|
63
69
|
// normalized contract).
|
|
64
70
|
const rawInputDetails = isRecord(raw.input_tokens_details)
|
|
65
71
|
? Object.fromEntries(Object.entries(raw.input_tokens_details as Record<string, unknown>)
|
|
66
|
-
.filter(([key]) => key !== "cache_write_tokens"))
|
|
72
|
+
.filter(([key]) => key !== "cache_write_tokens" && key !== "cache_write_1h_tokens"))
|
|
67
73
|
: {} as Record<string, unknown>;
|
|
68
74
|
const rawOutputDetails = isRecord(raw.output_tokens_details)
|
|
69
75
|
? raw.output_tokens_details as Record<string, unknown>
|
|
@@ -91,6 +97,11 @@ export function responsesUsage(usage: OcxUsage | undefined): Record<string, unkn
|
|
|
91
97
|
usage.cacheCreationInputTokens,
|
|
92
98
|
Math.max(0, inputTokens - cacheRead),
|
|
93
99
|
);
|
|
100
|
+
// The 1-hour-TTL share of the write (Anthropic cache_creation split), only when reported,
|
|
101
|
+
// so a translated Messages client can price it at the 1h rate.
|
|
102
|
+
if (usage.cacheCreation1hInputTokens !== undefined) {
|
|
103
|
+
inputDetails.cache_write_1h_tokens = Math.min(usage.cacheCreation1hInputTokens, inputDetails.cache_write_tokens as number);
|
|
104
|
+
}
|
|
94
105
|
}
|
|
95
106
|
out.input_tokens_details = inputDetails;
|
|
96
107
|
out.output_tokens_details = { ...rawOutputDetails, reasoning_tokens: usage.reasoningOutputTokens ?? 0 };
|
|
@@ -186,9 +197,12 @@ export function adapterFailureFromEvent(event: Extract<AdapterEvent, { type: "er
|
|
|
186
197
|
* This fixes items created from here on. History recorded before it is repaired at the
|
|
187
198
|
* replay boundary by `backfillWebSearchQueries()` in the Responses adapter.
|
|
188
199
|
*/
|
|
189
|
-
export function webSearchAction(queries: string[],
|
|
200
|
+
export function webSearchAction(queries: string[], anthropic?: AnthropicServerBlocks): Record<string, unknown> {
|
|
190
201
|
// Anthropic's server-side web_fetch opens one page instead of searching.
|
|
191
|
-
if (
|
|
202
|
+
if (anthropic?.use.name === "web_fetch") {
|
|
203
|
+
const url = (anthropic.result.content as { url?: unknown } | undefined)?.url ?? (anthropic.use.input as { url?: unknown }).url;
|
|
204
|
+
return { type: "open_page", url: typeof url === "string" ? url : "" };
|
|
205
|
+
}
|
|
192
206
|
const first = queries[0] ?? "";
|
|
193
207
|
return { type: "search", query: first, queries: queries.length > 0 ? queries : [first] };
|
|
194
208
|
}
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
type TranslatorBudget,
|
|
43
43
|
type TranslatorBufferKind,
|
|
44
44
|
} from "../lib/translator-budget";
|
|
45
|
-
import { adapterFailureFromEvent, emptyChunks, joinChunks, responsesUsage, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
|
|
45
|
+
import { adapterFailureFromEvent, emptyChunks, joinChunks, responsesUsage, toolCallArgumentsUsable, uuid, webSearchAction, webSearchCellItemId } from "./internal";
|
|
46
46
|
import type { OutputItem, StringChunks } from "./internal";
|
|
47
47
|
import { bridgeToResponsesSSE } from "./sse";
|
|
48
48
|
|
|
@@ -546,10 +546,10 @@ function buildResponseJSONWithBudget(
|
|
|
546
546
|
flushToolCall();
|
|
547
547
|
const safeSources = safeWebSearchSources(e.sources);
|
|
548
548
|
pushOutput({
|
|
549
|
-
type: "web_search_call", id:
|
|
550
|
-
action: webSearchAction(e.queries, e.
|
|
549
|
+
type: "web_search_call", id: webSearchCellItemId(e.itemId), status: e.status ?? "completed",
|
|
550
|
+
action: webSearchAction(e.queries, e.anthropic),
|
|
551
551
|
...(safeSources.length > 0 ? { sources: safeSources } : {}),
|
|
552
|
-
...(e.
|
|
552
|
+
...(e.anthropic ? { anthropic_blocks: e.anthropic } : {}),
|
|
553
553
|
});
|
|
554
554
|
if (safeSources.length > 0) {
|
|
555
555
|
for (const source of safeSources) {
|
package/src/bridge/sse.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { anthropicProgressItems } from "../responses/anthropic-progress";
|
|
2
2
|
import type {
|
|
3
3
|
AdapterEvent,
|
|
4
|
+
AnthropicServerBlocks,
|
|
4
5
|
OcxMessagePhase,
|
|
5
6
|
OcxProviderContinuationState,
|
|
6
7
|
OcxProviderOpaqueToolCallMetadata,
|
|
@@ -49,7 +50,7 @@ import {
|
|
|
49
50
|
type TranslatorBudget,
|
|
50
51
|
type TranslatorBufferKind,
|
|
51
52
|
} from "../lib/translator-budget";
|
|
52
|
-
import { adapterFailureFromEvent, emptyChunks, joinChunks, ownedBudgetAbandonedMs, responsesUsage, toolCallArgumentsCouldBeJson, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
|
|
53
|
+
import { adapterFailureFromEvent, emptyChunks, joinChunks, ownedBudgetAbandonedMs, responsesUsage, toolCallArgumentsCouldBeJson, toolCallArgumentsUsable, uuid, webSearchAction, webSearchCellItemId } from "./internal";
|
|
53
54
|
import type { OutputItem, StringChunks } from "./internal";
|
|
54
55
|
import { terminalRefusalFailure } from "./terminal-refusal";
|
|
55
56
|
|
|
@@ -685,15 +686,15 @@ export function bridgeToResponsesSSE(
|
|
|
685
686
|
// leaves a "Searching the web" spinner spinning forever.
|
|
686
687
|
// `sources` rides on the done item (additive field; codex-rs serde ignores unknown fields) so
|
|
687
688
|
// downstream translators (claude outbound) can fill web_search_tool_result content.
|
|
688
|
-
// `
|
|
689
|
-
//
|
|
690
|
-
const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[],
|
|
689
|
+
// `anthropic` (Anthropic's server-side call) rides verbatim as `anthropic_blocks`, for the same
|
|
690
|
+
// claude outbound translator; a web_fetch makes the cell a page open.
|
|
691
|
+
const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[], anthropic?: AnthropicServerBlocks) => {
|
|
691
692
|
if (!currentWebSearch) return;
|
|
692
693
|
const item = {
|
|
693
694
|
type: "web_search_call", id: currentWebSearch.itemId, status,
|
|
694
|
-
action: webSearchAction(queries,
|
|
695
|
+
action: webSearchAction(queries, anthropic),
|
|
695
696
|
...(sources && sources.length > 0 ? { sources } : {}),
|
|
696
|
-
...(
|
|
697
|
+
...(anthropic ? { anthropic_blocks: anthropic } : {}),
|
|
697
698
|
};
|
|
698
699
|
emit("response.output_item.done", { output_index: currentWebSearch.outputIndex, item });
|
|
699
700
|
retainFinishedItem(item as OutputItem);
|
|
@@ -769,9 +770,14 @@ export function bridgeToResponsesSSE(
|
|
|
769
770
|
returnIterator();
|
|
770
771
|
};
|
|
771
772
|
let handlingTranslatorOverflow = false;
|
|
772
|
-
terminateForTranslatorOverflow =
|
|
773
|
+
terminateForTranslatorOverflow = error => {
|
|
773
774
|
if (handlingTranslatorOverflow || terminated || clientCancelled || closed) return;
|
|
774
775
|
handlingTranslatorOverflow = true;
|
|
776
|
+
// An adapter-raised overflow event carries the usage the turn already consumed; the
|
|
777
|
+
// provider billed it, so it must not be logged as zero.
|
|
778
|
+
const usage = (error as AdapterEvent | null | undefined)?.type === "error"
|
|
779
|
+
? (error as Extract<AdapterEvent, { type: "error" }>).usage : undefined;
|
|
780
|
+
if (usage) options?.onUsage?.(usage);
|
|
775
781
|
abortCurrentToolCallForTranslatorOverflow();
|
|
776
782
|
currentWebSearch = null;
|
|
777
783
|
releasePendingWebSources();
|
|
@@ -787,6 +793,7 @@ export function bridgeToResponsesSSE(
|
|
|
787
793
|
sequence_number: seq++,
|
|
788
794
|
response: {
|
|
789
795
|
...responseSnapshot("failed", finishedItems),
|
|
796
|
+
...(usage ? { usage: responsesUsage(usage) } : {}),
|
|
790
797
|
error: failure,
|
|
791
798
|
last_error: failure,
|
|
792
799
|
},
|
|
@@ -1210,7 +1217,7 @@ export function bridgeToResponsesSSE(
|
|
|
1210
1217
|
flushHiddenRawReasoning();
|
|
1211
1218
|
if (currentToolCall) closeCurrentToolCall();
|
|
1212
1219
|
if (currentWebSearch) closeCurrentWebSearch("completed", []);
|
|
1213
|
-
const wsItemId =
|
|
1220
|
+
const wsItemId = webSearchCellItemId(event.itemId);
|
|
1214
1221
|
emit("response.output_item.added", {
|
|
1215
1222
|
output_index: outputIndex,
|
|
1216
1223
|
item: { type: "web_search_call", id: wsItemId, status: "in_progress" },
|
|
@@ -1223,7 +1230,7 @@ export function bridgeToResponsesSSE(
|
|
|
1223
1230
|
// (defensive), synthesize the added frame first so the done has a matching item.
|
|
1224
1231
|
if (!currentWebSearch || currentWebSearch.eventId !== event.id) {
|
|
1225
1232
|
if (currentWebSearch) closeCurrentWebSearch("completed", []);
|
|
1226
|
-
const wsItemId2 =
|
|
1233
|
+
const wsItemId2 = webSearchCellItemId(event.itemId);
|
|
1227
1234
|
emit("response.output_item.added", {
|
|
1228
1235
|
output_index: outputIndex,
|
|
1229
1236
|
item: { type: "web_search_call", id: wsItemId2, status: "in_progress" },
|
|
@@ -1231,7 +1238,7 @@ export function bridgeToResponsesSSE(
|
|
|
1231
1238
|
currentWebSearch = { itemId: wsItemId2, eventId: event.id, outputIndex };
|
|
1232
1239
|
}
|
|
1233
1240
|
const safeSources = safeWebSearchSources(event.sources);
|
|
1234
|
-
closeCurrentWebSearch(event.status ?? "completed", event.queries, safeSources, event.
|
|
1241
|
+
closeCurrentWebSearch(event.status ?? "completed", event.queries, safeSources, event.anthropic);
|
|
1235
1242
|
// Queue this search's sources for the next assistant message (dedup by URL).
|
|
1236
1243
|
if (safeSources.length > 0) {
|
|
1237
1244
|
for (const source of safeSources) {
|
|
@@ -87,6 +87,72 @@ export function declaredAnthropicServerTools(raw: unknown): Rec[] | undefined {
|
|
|
87
87
|
return tools.length > 0 ? tools : undefined;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/** Validate original call/result order without inventing inputs or pairing a later call backwards. */
|
|
91
|
+
function replayableServerRun(run: readonly Rec[]): boolean {
|
|
92
|
+
const pending = new Map<string, Rec>();
|
|
93
|
+
const completed = new Set<string>();
|
|
94
|
+
for (const block of run) {
|
|
95
|
+
if (block.type === "server_tool_use") {
|
|
96
|
+
const { id, name, input } = block;
|
|
97
|
+
if (typeof id !== "string" || !id.startsWith("srvtoolu_") || pending.has(id) || completed.has(id)
|
|
98
|
+
|| (name !== "web_search" && name !== "web_fetch") || !isRec(input)) return false;
|
|
99
|
+
const prototype = Object.getPrototypeOf(input);
|
|
100
|
+
const required = input[name === "web_search" ? "query" : "url"];
|
|
101
|
+
if ((prototype !== Object.prototype && prototype !== null)
|
|
102
|
+
|| typeof required !== "string" || required.trim().length === 0) return false;
|
|
103
|
+
pending.set(id, block);
|
|
104
|
+
} else {
|
|
105
|
+
const id = block.tool_use_id;
|
|
106
|
+
if (typeof id !== "string") return false;
|
|
107
|
+
const call = pending.get(id);
|
|
108
|
+
if (!call || block.type !== `${call.name}_tool_result`
|
|
109
|
+
|| (Array.isArray(block.content) && !block.content.every(h => isRec(h) && typeof h.encrypted_content === "string"))) return false;
|
|
110
|
+
pending.delete(id);
|
|
111
|
+
completed.add(id);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return completed.size > 0 && pending.size === 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Completed Anthropic server-tool exchanges (`server_tool_use` + `web_search_tool_result` /
|
|
119
|
+
* `web_fetch_tool_result`) in the caller's assistant history. The Responses body drops them, so
|
|
120
|
+
* an Anthropic destination re-inserts them out of body, keyed by the block that follows each run
|
|
121
|
+
* in its message: `call:<tool_use id>`, `text:<text>`, `thinking:<signature>` or
|
|
122
|
+
* `redacted:<data>`, all of which survive translation. An anchor that occurs more than once in the
|
|
123
|
+
* assistant history (string content included) cannot name one owner, so its run stays dropped,
|
|
124
|
+
* as it was before replay existed. A run with none, an unpaired call, a
|
|
125
|
+
* non-Anthropic id, or a search hit without `encrypted_content` is not replayable and stays dropped.
|
|
126
|
+
*/
|
|
127
|
+
export function replayedAnthropicServerBlocks(raw: unknown): Map<string, Rec[]> | undefined {
|
|
128
|
+
if (!isRec(raw) || !Array.isArray(raw.messages)) return undefined;
|
|
129
|
+
const out = new Map<string, Rec[]>();
|
|
130
|
+
const seen = new Map<string, number>();
|
|
131
|
+
const count = (anchor: string) => seen.set(anchor, (seen.get(anchor) ?? 0) + 1);
|
|
132
|
+
for (const message of raw.messages) {
|
|
133
|
+
if (!isRec(message) || message.role !== "assistant") continue;
|
|
134
|
+
if (typeof message.content === "string") count(`text:${message.content}`);
|
|
135
|
+
if (!Array.isArray(message.content)) continue;
|
|
136
|
+
let run: Rec[] = [];
|
|
137
|
+
for (const block of message.content.filter(isRec)) {
|
|
138
|
+
if (block.type === "server_tool_use" || block.type === "web_search_tool_result" || block.type === "web_fetch_tool_result") {
|
|
139
|
+
// The caller's breakpoints are dropped on this lane, as for the declared tools above.
|
|
140
|
+
run.push({ ...block, cache_control: undefined });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const anchor = block.type === "tool_use" ? `call:${String(block.id)}`
|
|
144
|
+
: block.type === "text" ? `text:${String(block.text)}`
|
|
145
|
+
: block.type === "thinking" ? `thinking:${String(block.signature)}`
|
|
146
|
+
: block.type === "redacted_thinking" ? `redacted:${String(block.data)}` : undefined;
|
|
147
|
+
if (anchor) count(anchor);
|
|
148
|
+
if (anchor && replayableServerRun(run)) out.set(anchor, run);
|
|
149
|
+
run = [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const anchor of out.keys()) if ((seen.get(anchor) ?? 0) > 1) out.delete(anchor);
|
|
153
|
+
return out.size > 0 ? out : undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
90
156
|
export function toolChoiceToResponses(choice: unknown, body: Rec): void {
|
|
91
157
|
if (!isRec(choice)) return;
|
|
92
158
|
if (choice.disable_parallel_tool_use === true) body.parallel_tool_calls = false;
|