@bitkyc08/opencodex 2.20.0 → 2.21.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/gui/dist/assets/{index-DSK3S5HY.js → index-BOFeam5a.js} +2 -2
- package/gui/dist/assets/{index-DF_UFrGS.css → index-Xq49CY8F.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +2 -28
- package/src/adapters/google.ts +31 -3
- package/src/adapters/openai-chat.ts +25 -8
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/bridge.ts +15 -2
- package/src/claude/gateway-cache.ts +41 -4
- package/src/cli/claude.ts +1 -1
- package/src/generated/compatibility-version.json +24 -16
- package/src/images/loop.ts +15 -5
- package/src/providers/registry.ts +7 -1
- package/src/responses/custom-tool-compat.ts +4 -1
- package/src/responses/parser.ts +7 -1
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/schema.ts +6 -0
- package/src/server/auth-cors.ts +42 -6
- package/src/server/system-env.ts +1 -1
- package/src/types.ts +18 -1
- package/src/web-search/loop.ts +21 -5
package/src/images/loop.ts
CHANGED
|
@@ -14,8 +14,9 @@ import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "../adapters/
|
|
|
14
14
|
import { existsSync } from "node:fs";
|
|
15
15
|
import { pathToFileURL } from "node:url";
|
|
16
16
|
import { createAdapterEventQueue } from "../adapters/run-turn-queue";
|
|
17
|
-
import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderContinuationState, OcxRequestOptions, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
|
|
17
|
+
import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderContinuationState, OcxProviderOpaqueToolCallMetadata, OcxRequestOptions, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
|
|
18
18
|
import { namespacedToolName, toolChoiceToolPredicate } from "../types";
|
|
19
|
+
import { cloneProviderOpaqueToolCallMetadata } from "../responses/provider-opaque-metadata";
|
|
19
20
|
import type { AttemptRecoveryKind } from "../usage/log";
|
|
20
21
|
import { bridgeToResponsesSSE } from "../bridge";
|
|
21
22
|
import { clearableDeadline, idleDeadline } from "../lib/abort";
|
|
@@ -93,6 +94,11 @@ interface ImageCall {
|
|
|
93
94
|
id: string;
|
|
94
95
|
name: string;
|
|
95
96
|
args: string;
|
|
97
|
+
/**
|
|
98
|
+
* Provider-opaque metadata from the originating part (issue #1735). Stored PER CALL: a
|
|
99
|
+
* signature belongs to one specific part, so parallel calls must not share one value.
|
|
100
|
+
*/
|
|
101
|
+
providerMetadata?: OcxProviderOpaqueToolCallMetadata;
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
/**
|
|
@@ -109,13 +115,13 @@ function scanEventsForImageCall(events: AdapterEvent[], toolNames: Set<string>):
|
|
|
109
115
|
const calls: ImageCall[] = [];
|
|
110
116
|
const passthrough: AdapterEvent[] = [];
|
|
111
117
|
let hasRealToolCall = false;
|
|
112
|
-
let pending: { name: string; id: string; argsBuf: string; events: AdapterEvent[] } | null = null;
|
|
118
|
+
let pending: { name: string; id: string; argsBuf: string; events: AdapterEvent[]; providerMetadata?: OcxProviderOpaqueToolCallMetadata } | null = null;
|
|
113
119
|
const flushPending = (): void => {
|
|
114
120
|
if (!pending) return;
|
|
115
121
|
if (toolNames.has(pending.name)) {
|
|
116
122
|
// Unterminated image call still carries buffered args — fulfill so malformed JSON
|
|
117
123
|
// becomes a normal tool_result error instead of silently vanishing.
|
|
118
|
-
calls.push({ id: pending.id, name: pending.name, args: pending.argsBuf });
|
|
124
|
+
calls.push({ id: pending.id, name: pending.name, args: pending.argsBuf, providerMetadata: pending.providerMetadata });
|
|
119
125
|
} else {
|
|
120
126
|
passthrough.push(...pending.events);
|
|
121
127
|
hasRealToolCall = true;
|
|
@@ -125,14 +131,14 @@ function scanEventsForImageCall(events: AdapterEvent[], toolNames: Set<string>):
|
|
|
125
131
|
for (const e of events) {
|
|
126
132
|
if (e.type === "tool_call_start") {
|
|
127
133
|
flushPending();
|
|
128
|
-
pending = { name: e.name, id: e.id, argsBuf: "", events: [e] };
|
|
134
|
+
pending = { name: e.name, id: e.id, argsBuf: "", events: [e], providerMetadata: e.providerMetadata };
|
|
129
135
|
} else if (e.type === "tool_call_delta" && pending) {
|
|
130
136
|
pending.argsBuf += e.arguments;
|
|
131
137
|
pending.events.push(e);
|
|
132
138
|
} else if (e.type === "tool_call_end" && pending) {
|
|
133
139
|
pending.events.push(e);
|
|
134
140
|
if (toolNames.has(pending.name)) {
|
|
135
|
-
calls.push({ id: pending.id, name: pending.name, args: pending.argsBuf });
|
|
141
|
+
calls.push({ id: pending.id, name: pending.name, args: pending.argsBuf, providerMetadata: pending.providerMetadata });
|
|
136
142
|
} else {
|
|
137
143
|
passthrough.push(...pending.events);
|
|
138
144
|
hasRealToolCall = true;
|
|
@@ -871,6 +877,10 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
871
877
|
id: call.id,
|
|
872
878
|
name: call.name,
|
|
873
879
|
arguments: args,
|
|
880
|
+
// Clone per call: parallel media calls each keep their own signature.
|
|
881
|
+
...(cloneProviderOpaqueToolCallMetadata(call.providerMetadata)
|
|
882
|
+
? { providerMetadata: cloneProviderOpaqueToolCallMetadata(call.providerMetadata) }
|
|
883
|
+
: {}),
|
|
874
884
|
})),
|
|
875
885
|
],
|
|
876
886
|
timestamp: now,
|
|
@@ -1977,7 +1977,11 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1977
1977
|
// 260710 GLM-5.2 context and path-specific ids: Tier-2 evidence in
|
|
1978
1978
|
// devlog/_plan/260710_provider_hardening/002_research_cn.md.
|
|
1979
1979
|
// 260814: glm-5.3 / glm-5.3[1m] added per docs.z.ai/devpack/latest-model, which lists them as
|
|
1980
|
-
// Coding Plan ids on this same endpoint.
|
|
1980
|
+
// Coding Plan ids on this same endpoint.
|
|
1981
|
+
// 260815: docs.z.ai/guides/llm/glm-5.3 now publishes the capability table (thinking, streaming,
|
|
1982
|
+
// function calling, caching, structured output) and a 128K output budget, recorded here as the
|
|
1983
|
+
// exact 131_072 every other source in this repo uses for that model. Coding Plan pricing stays
|
|
1984
|
+
// unpublished, so no cost entry is asserted.
|
|
1981
1985
|
{
|
|
1982
1986
|
id: "zai", label: "Z.AI — GLM Coding Plan", baseUrl: "https://api.z.ai/api/coding/paas/v4", adapter: "openai-chat", authKind: "key",
|
|
1983
1987
|
dashboardUrl: "https://z.ai/manage-apikey/apikey-list", defaultModel: "glm-5.3",
|
|
@@ -1988,6 +1992,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1988
1992
|
modelSuffixBracketStrip: true,
|
|
1989
1993
|
noVisionModels: ZAI_GLM_5X_MODELS,
|
|
1990
1994
|
modelReasoningEfforts: ZAI_GLM_5X_REASONING_EFFORTS,
|
|
1995
|
+
modelDefaultReasoningEfforts: Object.fromEntries(ZAI_GLM_53_MODELS.map(id => [id, "max"])),
|
|
1996
|
+
modelMaxOutputTokens: Object.fromEntries(ZAI_GLM_53_MODELS.map(id => [id, 131_072])),
|
|
1991
1997
|
modelSupportsReasoningSummaries: Object.fromEntries(ZAI_GLM_5X_MODELS.map(id => [id, true])),
|
|
1992
1998
|
preserveReasoningContentModels: ZAI_GLM_5X_MODELS,
|
|
1993
1999
|
},
|
|
@@ -70,6 +70,9 @@ function rewriteForUpstream(
|
|
|
70
70
|
|| isPlainObject(value.format)
|
|
71
71
|
|| isPlainObject(value.parameters);
|
|
72
72
|
if (!isDefinition) return { ...rest, type: "function" };
|
|
73
|
+
const inputDescription = value.name === "exec"
|
|
74
|
+
? "JavaScript source for unified exec. Use await tools.exec_command(...) for shell commands and text(...) to return textual output; do not provide a bare shell command."
|
|
75
|
+
: "Raw input for this client-executed custom tool.";
|
|
73
76
|
return {
|
|
74
77
|
...rest,
|
|
75
78
|
type: "function",
|
|
@@ -78,7 +81,7 @@ function rewriteForUpstream(
|
|
|
78
81
|
properties: {
|
|
79
82
|
input: {
|
|
80
83
|
type: "string",
|
|
81
|
-
description:
|
|
84
|
+
description: inputDescription,
|
|
82
85
|
},
|
|
83
86
|
},
|
|
84
87
|
required: ["input"],
|
package/src/responses/parser.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
} from "../types";
|
|
13
13
|
import { namespacedToolName } from "../types";
|
|
14
14
|
import { responsesRequestSchema } from "./schema";
|
|
15
|
+
import { providerMetadataFromResponsesFunctionCall } from "./provider-opaque-metadata";
|
|
15
16
|
import { compactionItemToText } from "./compaction";
|
|
16
17
|
import { previousResponseReplayPrefixLength } from "./state";
|
|
17
18
|
import { decodeReasoningEnvelope } from "./reasoning-envelope";
|
|
@@ -498,7 +499,7 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
498
499
|
}
|
|
499
500
|
|
|
500
501
|
if (effectiveType === "function_call") {
|
|
501
|
-
const call = item as { id?: string; call_id: string; name: string; arguments?: string; namespace?: string };
|
|
502
|
+
const call = item as { id?: string; call_id: string; name: string; arguments?: string; namespace?: string; extra_content?: unknown };
|
|
502
503
|
// Tolerate empty/non-JSON arguments (e.g. a no-arg tool call serialized as "") instead of
|
|
503
504
|
// throwing — a single poisoned history item would otherwise 400 every subsequent turn.
|
|
504
505
|
let args: Record<string, unknown> = {};
|
|
@@ -519,6 +520,11 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
519
520
|
type: "toolCall", id: call.call_id, name: call.name, arguments: args,
|
|
520
521
|
...(call.namespace ? { namespace: call.namespace } : {}),
|
|
521
522
|
};
|
|
523
|
+
// Provider-opaque metadata (e.g. a Gemini thought signature) travels with the call so a
|
|
524
|
+
// history-replayed or previous_response_id turn rebuilds the same signed part instead of
|
|
525
|
+
// depending on the same-process replay cache (issue #1735).
|
|
526
|
+
const providerMetadata = providerMetadataFromResponsesFunctionCall(call);
|
|
527
|
+
if (providerMetadata) toolCall.providerMetadata = providerMetadata;
|
|
522
528
|
assistantHolderWithReasoning().content.push(toolCall);
|
|
523
529
|
continue;
|
|
524
530
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-opaque tool-call metadata across the Responses boundary (issue #1735).
|
|
3
|
+
*
|
|
4
|
+
* Gemini issues a `thoughtSignature` on the exact part that carries a function call, and the
|
|
5
|
+
* next request is only valid if that signature comes back on the part rebuilt from that same
|
|
6
|
+
* call. Every synthetic loop in this proxy (web search, images, continuation replay) tears a
|
|
7
|
+
* tool call down into id/name/arguments and builds a fresh one, which silently dropped the
|
|
8
|
+
* signature and left only the same-process replay cache to paper over it. History replay and
|
|
9
|
+
* `previous_response_id` had no cache to fall back on.
|
|
10
|
+
*
|
|
11
|
+
* This module is the single seam where that metadata crosses into and out of the Responses
|
|
12
|
+
* wire, so a loop that rebuilds a call only has to carry one field instead of knowing about
|
|
13
|
+
* any provider. Values are treated as opaque: never parsed, merged, re-encoded, or synthesized.
|
|
14
|
+
*/
|
|
15
|
+
import type { OcxProviderOpaqueToolCallMetadata } from "../types";
|
|
16
|
+
|
|
17
|
+
/** Wire shape: `extra_content.google.thought_signature` on a Responses function_call item. */
|
|
18
|
+
interface ResponsesExtraContent {
|
|
19
|
+
google?: { thought_signature?: unknown };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isObj(value: unknown): value is Record<string, unknown> {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Same ceiling the Antigravity replay cache already enforces on a stored signature. An opaque
|
|
28
|
+
* token this large is not a real signature, and accepting it would let a caller push unbounded
|
|
29
|
+
* state through history replay.
|
|
30
|
+
*/
|
|
31
|
+
const MAX_SIGNATURE_BYTES = 64 * 1024;
|
|
32
|
+
|
|
33
|
+
function isCarryableSignature(value: unknown): value is string {
|
|
34
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
35
|
+
// Cheap length pre-check: UTF-8 is at most 3 bytes per UTF-16 code unit for the BMP, so this
|
|
36
|
+
// skips the encode for the overwhelmingly common short case.
|
|
37
|
+
if (value.length <= MAX_SIGNATURE_BYTES / 3) return true;
|
|
38
|
+
return Buffer.byteLength(value, "utf8") <= MAX_SIGNATURE_BYTES;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Read provider metadata off an inbound Responses function_call item. */
|
|
42
|
+
export function providerMetadataFromResponsesFunctionCall(
|
|
43
|
+
item: { extra_content?: unknown } | undefined,
|
|
44
|
+
): OcxProviderOpaqueToolCallMetadata | undefined {
|
|
45
|
+
const extra = item?.extra_content;
|
|
46
|
+
if (!isObj(extra)) return undefined;
|
|
47
|
+
const google = (extra as ResponsesExtraContent).google;
|
|
48
|
+
if (!isObj(google)) return undefined;
|
|
49
|
+
const signature = google.thought_signature;
|
|
50
|
+
if (!isCarryableSignature(signature)) return undefined;
|
|
51
|
+
return { google: { thoughtSignature: signature } };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Serialize provider metadata onto an outbound Responses function_call item. */
|
|
55
|
+
export function responsesExtraContentFromProviderMetadata(
|
|
56
|
+
metadata: OcxProviderOpaqueToolCallMetadata | undefined,
|
|
57
|
+
): { extra_content: { google: { thought_signature: string } } } | undefined {
|
|
58
|
+
const signature = metadata?.google?.thoughtSignature;
|
|
59
|
+
if (!isCarryableSignature(signature)) return undefined;
|
|
60
|
+
return { extra_content: { google: { thought_signature: signature } } };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Copy metadata for a rebuilt tool call. A signature belongs to one specific part, so a loop
|
|
65
|
+
* that fans one model response into several calls must copy per call and never share or merge.
|
|
66
|
+
*/
|
|
67
|
+
export function cloneProviderOpaqueToolCallMetadata(
|
|
68
|
+
metadata: OcxProviderOpaqueToolCallMetadata | undefined,
|
|
69
|
+
): OcxProviderOpaqueToolCallMetadata | undefined {
|
|
70
|
+
const signature = metadata?.google?.thoughtSignature;
|
|
71
|
+
if (!isCarryableSignature(signature)) return undefined;
|
|
72
|
+
return { google: { thoughtSignature: signature } };
|
|
73
|
+
}
|
package/src/responses/schema.ts
CHANGED
|
@@ -64,6 +64,12 @@ const functionCallItemSchema = z.object({
|
|
|
64
64
|
name: z.string().min(1),
|
|
65
65
|
namespace: z.string().optional(),
|
|
66
66
|
arguments: z.string().optional(),
|
|
67
|
+
// Provider-opaque metadata that must survive the round trip verbatim (issue #1735). The shape
|
|
68
|
+
// is bounded on purpose: only the one nested key we round-trip is modeled, so an unexpected
|
|
69
|
+
// payload cannot ride through as arbitrary passthrough state.
|
|
70
|
+
extra_content: z.object({
|
|
71
|
+
google: z.object({ thought_signature: z.string().optional() }).optional(),
|
|
72
|
+
}).optional(),
|
|
67
73
|
});
|
|
68
74
|
const functionCallOutputItemSchema = z.object({
|
|
69
75
|
type: z.literal("function_call_output"),
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -140,17 +140,53 @@ export function browserSecurityHeaders(): Record<string, string> {
|
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Baseline data-plane request headers. ChatGPT-Account-Id is required for browser/Electron
|
|
145
|
+
* ChatGPT & Codex App voice preflights (direct forward auth matches the bearer to this account
|
|
146
|
+
* id). The OpenAI-Alpha .. X-OAI-Attestation block covers GPT-Live voice protocol headers
|
|
147
|
+
* relayed by the /v1/live call-create path.
|
|
148
|
+
*/
|
|
149
|
+
const STATIC_ALLOWED_REQUEST_HEADERS =
|
|
150
|
+
"Content-Type, Authorization, X-OpenCodex-API-Key, X-Api-Key, Anthropic-Version, Anthropic-Beta, ChatGPT-Account-Id, OpenAI-Alpha, X-Session-Id, Session-Id, Thread-Id, Originator, X-OAI-Attestation";
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A fixed allow-list cannot enumerate vendor telemetry headers: the OpenAI and Anthropic
|
|
154
|
+
* browser SDKs send `X-Stainless-*` describing runtime and retry state, and the browser blocks
|
|
155
|
+
* the real request when the preflight omits even one of them (#1773).
|
|
156
|
+
*
|
|
157
|
+
* Echo what an already-allowed origin asked for, and fall back to the static list otherwise.
|
|
158
|
+
* The echo is deliberately gated on the origin check that ran first: this widens which headers
|
|
159
|
+
* an admitted caller may send, never which origins are admitted, and it grants nothing to an
|
|
160
|
+
* origin that would have been rejected anyway. Authentication is unchanged — the preflight
|
|
161
|
+
* itself carries no credential and produces no auth or account-pool side effect.
|
|
162
|
+
*/
|
|
163
|
+
function allowedRequestHeaders(req?: Request): string {
|
|
164
|
+
const requested = req?.headers.get("Access-Control-Request-Headers")?.trim();
|
|
165
|
+
if (!requested) return STATIC_ALLOWED_REQUEST_HEADERS;
|
|
166
|
+
const seen = new Set(STATIC_ALLOWED_REQUEST_HEADERS.split(",").map(h => h.trim().toLowerCase()));
|
|
167
|
+
const extra: string[] = [];
|
|
168
|
+
for (const raw of requested.split(",")) {
|
|
169
|
+
const name = raw.trim();
|
|
170
|
+
// Header names are case-insensitive on the wire, so normalize before de-duplicating;
|
|
171
|
+
// echo the caller's spelling for the ones we add.
|
|
172
|
+
if (!name || seen.has(name.toLowerCase())) continue;
|
|
173
|
+
seen.add(name.toLowerCase());
|
|
174
|
+
extra.push(name);
|
|
175
|
+
}
|
|
176
|
+
return extra.length === 0 ? STATIC_ALLOWED_REQUEST_HEADERS : `${STATIC_ALLOWED_REQUEST_HEADERS}, ${extra.join(", ")}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
143
179
|
export function corsHeaders(req?: Request, config?: RequestPolicyView): Record<string, string> {
|
|
144
180
|
const origin = req?.headers.get("Origin");
|
|
145
|
-
const
|
|
181
|
+
const originAllowed = Boolean(origin && req && config && isAllowedRequestOrigin(req, config));
|
|
182
|
+
const allowOrigin = originAllowed && origin ? origin : _corsOrigin;
|
|
146
183
|
return {
|
|
147
184
|
"Access-Control-Allow-Origin": allowOrigin,
|
|
148
185
|
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
"
|
|
153
|
-
"Vary": "Origin",
|
|
186
|
+
"Access-Control-Allow-Headers": allowedRequestHeaders(originAllowed ? req : undefined),
|
|
187
|
+
// A response that varies by the request's headers must say so, or a shared cache can
|
|
188
|
+
// replay one client's allow-list to a client that asked for different headers.
|
|
189
|
+
"Vary": "Origin, Access-Control-Request-Headers",
|
|
154
190
|
...browserSecurityHeaders(),
|
|
155
191
|
};
|
|
156
192
|
}
|
package/src/server/system-env.ts
CHANGED
|
@@ -335,7 +335,7 @@ export async function injectSystemEnv(port: number, config: OcxConfig): Promise<
|
|
|
335
335
|
// without a token — keep it in sync with this proxy's /v1/models. Best-effort.
|
|
336
336
|
try {
|
|
337
337
|
const { refreshGatewayModelCacheFromProxy } = await import("../claude/gateway-cache");
|
|
338
|
-
await refreshGatewayModelCacheFromProxy(port);
|
|
338
|
+
await refreshGatewayModelCacheFromProxy(port, { admissionConfig: config });
|
|
339
339
|
} catch { /* best-effort */ }
|
|
340
340
|
|
|
341
341
|
// Roster agent definitions (devlog 070): same launch-time sync for plain `claude`.
|
package/src/types.ts
CHANGED
|
@@ -180,10 +180,27 @@ export interface OcxToolCall {
|
|
|
180
180
|
arguments: Record<string, unknown>;
|
|
181
181
|
customWireName?: string;
|
|
182
182
|
thoughtSignature?: string;
|
|
183
|
+
/**
|
|
184
|
+
* Provider-issued opaque metadata that must survive the whole round trip unchanged
|
|
185
|
+
* (issue #1735). A signed Gemini part is only valid when its signature comes back on the
|
|
186
|
+
* SAME part it was issued for, so this travels with the individual tool call rather than
|
|
187
|
+
* being matched by name/arguments after the fact.
|
|
188
|
+
*/
|
|
189
|
+
providerMetadata?: OcxProviderOpaqueToolCallMetadata;
|
|
183
190
|
/** MCP namespace (e.g. "mcp__context7") when this call targets a namespaced tool. */
|
|
184
191
|
namespace?: string;
|
|
185
192
|
}
|
|
186
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Opaque, provider-scoped tool-call metadata. Values are never parsed, merged, re-encoded, or
|
|
196
|
+
* synthesized — they are carried verbatim or not at all.
|
|
197
|
+
*/
|
|
198
|
+
export interface OcxProviderOpaqueToolCallMetadata {
|
|
199
|
+
google?: {
|
|
200
|
+
thoughtSignature?: string;
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
187
204
|
export type OcxAssistantContentPart = OcxTextContent | OcxThinkingContent | OcxToolCall;
|
|
188
205
|
|
|
189
206
|
export interface OcxTool {
|
|
@@ -328,7 +345,7 @@ export type AdapterEvent =
|
|
|
328
345
|
// Never rendered — it only rides the reasoning item's envelope so the next request can replay it.
|
|
329
346
|
| { type: "kiro_redacted_reasoning"; data: string }
|
|
330
347
|
| { type: "reasoning_raw_delta"; text: string }
|
|
331
|
-
| { type: "tool_call_start"; id: string; name: string }
|
|
348
|
+
| { type: "tool_call_start"; id: string; name: string; providerMetadata?: OcxProviderOpaqueToolCallMetadata }
|
|
332
349
|
| { type: "tool_call_delta"; arguments: string }
|
|
333
350
|
| { type: "tool_call_end" }
|
|
334
351
|
/** Internal boundary between a guarded first pass and its one-shot continuation. */
|
package/src/web-search/loop.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "../adapters/base";
|
|
2
|
-
import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
|
|
2
|
+
import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxProviderOpaqueToolCallMetadata, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
|
|
3
3
|
import { namespacedToolName, toolChoiceToolPredicate } from "../types";
|
|
4
|
+
import { cloneProviderOpaqueToolCallMetadata } from "../responses/provider-opaque-metadata";
|
|
4
5
|
import type { AttemptRecoveryKind } from "../usage/log";
|
|
5
6
|
import { bridgeToResponsesSSE } from "../bridge";
|
|
6
7
|
import { runWebSearch, type SidecarOutcome, type SidecarOutcomeRecorder, type SidecarSettings } from "./executor";
|
|
@@ -32,6 +33,11 @@ interface WebSearchCall {
|
|
|
32
33
|
// empty array means the model called the tool with neither `query` nor `queries` (handled as an
|
|
33
34
|
// empty-query placeholder).
|
|
34
35
|
queries: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Provider-opaque metadata from the originating part (issue #1735). Stored PER CALL so a
|
|
38
|
+
* signature can never migrate to a different call when the model batches several.
|
|
39
|
+
*/
|
|
40
|
+
providerMetadata?: OcxProviderOpaqueToolCallMetadata;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/**
|
|
@@ -69,7 +75,7 @@ export function scanEventsForWebSearch(events: AdapterEvent[]): {
|
|
|
69
75
|
const passthrough: AdapterEvent[] = [];
|
|
70
76
|
let hasRealToolCall = false;
|
|
71
77
|
let hasMalformedToolCall = false;
|
|
72
|
-
let pending: { name: string; id: string; argsBuf: string; closed: boolean; events: AdapterEvent[] } | null = null;
|
|
78
|
+
let pending: { name: string; id: string; argsBuf: string; closed: boolean; events: AdapterEvent[]; providerMetadata?: OcxProviderOpaqueToolCallMetadata } | null = null;
|
|
73
79
|
const isBlank = (value: string): boolean => value.trim().length === 0;
|
|
74
80
|
const flushPending = (): void => {
|
|
75
81
|
// A pending call that never saw tool_call_end is structurally malformed.
|
|
@@ -84,7 +90,7 @@ export function scanEventsForWebSearch(events: AdapterEvent[]): {
|
|
|
84
90
|
if (e.type === "tool_call_start") {
|
|
85
91
|
flushPending();
|
|
86
92
|
if (isBlank(e.id) || isBlank(e.name)) hasMalformedToolCall = true;
|
|
87
|
-
pending = { name: e.name, id: e.id, argsBuf: "", closed: false, events: [e] };
|
|
93
|
+
pending = { name: e.name, id: e.id, argsBuf: "", closed: false, events: [e], providerMetadata: e.providerMetadata };
|
|
88
94
|
} else if (e.type === "tool_call_delta") {
|
|
89
95
|
// Orphan delta (no open call) is malformed.
|
|
90
96
|
if (!pending) hasMalformedToolCall = true;
|
|
@@ -100,7 +106,7 @@ export function scanEventsForWebSearch(events: AdapterEvent[]): {
|
|
|
100
106
|
pending.events.push(e);
|
|
101
107
|
pending.closed = true;
|
|
102
108
|
if (pending.name === WEB_SEARCH_TOOL_NAME) {
|
|
103
|
-
calls.push({ id: pending.id, queries: parseQueries(pending.argsBuf) });
|
|
109
|
+
calls.push({ id: pending.id, queries: parseQueries(pending.argsBuf), providerMetadata: pending.providerMetadata });
|
|
104
110
|
} else {
|
|
105
111
|
passthrough.push(...pending.events);
|
|
106
112
|
if (!isBlank(pending.id) && !isBlank(pending.name)) hasRealToolCall = true;
|
|
@@ -678,7 +684,17 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
678
684
|
// Signed thinking must precede tool_use on replay (Anthropic extended thinking), and
|
|
679
685
|
// unsigned raw reasoning has to ride along for providers that require it back (#688).
|
|
680
686
|
...precedingThinking,
|
|
681
|
-
{
|
|
687
|
+
{
|
|
688
|
+
type: "toolCall" as const,
|
|
689
|
+
id: call.id,
|
|
690
|
+
name: WEB_SEARCH_TOOL_NAME,
|
|
691
|
+
arguments: callArgs,
|
|
692
|
+
// Re-attach the signature to the rebuilt call so a sidecar turn keeps Gemini
|
|
693
|
+
// reasoning continuity instead of relying on the same-process replay cache.
|
|
694
|
+
...(cloneProviderOpaqueToolCallMetadata(call.providerMetadata)
|
|
695
|
+
? { providerMetadata: cloneProviderOpaqueToolCallMetadata(call.providerMetadata) }
|
|
696
|
+
: {}),
|
|
697
|
+
},
|
|
682
698
|
],
|
|
683
699
|
timestamp: now,
|
|
684
700
|
});
|