mixdog 0.9.50 → 0.9.52
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/package.json +5 -3
- package/scripts/abort-recovery-test.mjs +17 -1
- package/scripts/agent-model-liveness-test.mjs +79 -2
- package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
- package/scripts/anthropic-transport-policy-test.mjs +466 -0
- package/scripts/atomic-lock-tryonce-test.mjs +60 -1
- package/scripts/build-tui.mjs +13 -1
- package/scripts/channel-daemon-smoke.mjs +630 -10
- package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
- package/scripts/code-graph-disk-hit-test.mjs +11 -0
- package/scripts/code-graph-root-federation-test.mjs +273 -0
- package/scripts/compact-pressure-test.mjs +55 -1
- package/scripts/compact-smoke.mjs +80 -0
- package/scripts/context-mcp-metering-test.mjs +1350 -19
- package/scripts/deferred-tool-loading-test.mjs +17 -0
- package/scripts/gemini-provider-test.mjs +1053 -0
- package/scripts/hook-bus-test.mjs +23 -0
- package/scripts/internal-tools-normalization-test.mjs +10 -0
- package/scripts/interrupted-turn-history-test.mjs +371 -0
- package/scripts/lifecycle-api-test.mjs +76 -0
- package/scripts/max-output-recovery-test.mjs +55 -0
- package/scripts/mcp-grace-deferred-test.mjs +89 -13
- package/scripts/memory-pg-recovery-test.mjs +59 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +391 -4
- package/scripts/process-lifecycle-test.mjs +389 -0
- package/scripts/provider-admission-scheduler-test.mjs +582 -0
- package/scripts/provider-contract-test.mjs +268 -0
- package/scripts/provider-toolcall-test.mjs +520 -3
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/session-bench-cache-break-test.mjs +102 -0
- package/scripts/session-bench.mjs +101 -42
- package/scripts/shell-failure-diagnostics-test.mjs +73 -4
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/smoke-loop-failure-summary-test.mjs +38 -0
- package/scripts/smoke-loop-failure-summary.mjs +16 -0
- package/scripts/smoke-loop.mjs +2 -7
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/tool-failures.mjs +15 -1
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +43 -7
- package/scripts/web-fetch-routing-test.mjs +158 -0
- package/src/cli.mjs +15 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -0
- package/src/runtime/agent/orchestrator/internal-tools.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +95 -46
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +144 -51
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +74 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +111 -6
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +224 -104
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +127 -55
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -7
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +24 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -14
- package/src/runtime/memory/index.mjs +22 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
- package/src/runtime/memory/lib/pg/process.mjs +91 -47
- package/src/runtime/memory/lib/pg/supervisor.mjs +50 -13
- package/src/runtime/search/index.mjs +41 -0
- package/src/runtime/search/lib/http-fetch.mjs +154 -0
- package/src/runtime/search/tool-defs.mjs +23 -0
- package/src/runtime/shared/atomic-file.mjs +28 -150
- package/src/runtime/shared/process-lifecycle.mjs +363 -0
- package/src/runtime/shared/process-shutdown.mjs +27 -4
- package/src/runtime/shared/resource-admission.mjs +359 -0
- package/src/runtime/shared/staged-child-result.mjs +19 -0
- package/src/session-runtime/context-status.mjs +38 -27
- package/src/session-runtime/lifecycle-api.mjs +26 -6
- package/src/session-runtime/provider-request-tools.mjs +72 -0
- package/src/session-runtime/runtime-core.mjs +43 -18
- package/src/session-runtime/session-turn-api.mjs +5 -4
- package/src/session-runtime/tool-catalog.mjs +375 -15
- package/src/standalone/agent-tool.mjs +17 -38
- package/src/standalone/channel-daemon-client.mjs +200 -38
- package/src/standalone/channel-daemon-transport.mjs +136 -9
- package/src/standalone/channel-worker.mjs +79 -12
- package/src/standalone/hook-bus/handlers.mjs +4 -0
- package/src/standalone/hook-bus/rules.mjs +7 -1
- package/src/standalone/hook-bus.mjs +10 -2
- package/src/tui/App.jsx +17 -11
- package/src/tui/app/live-spinner-visibility.mjs +20 -0
- package/src/tui/dist/index.mjs +101 -281
- package/src/tui/engine/session-api-ext.mjs +13 -2
- package/src/tui/engine/session-api.mjs +1 -1
- package/src/tui/engine/turn.mjs +10 -6
- package/src/tui/engine.mjs +13 -4
- package/src/tui/index.jsx +4 -1
- package/src/tui/lib/voice-setup.mjs +16 -0
|
@@ -12,6 +12,91 @@ import { SchemaType } from '@google/generative-ai';
|
|
|
12
12
|
import { traceHash, stableTraceStringify } from './trace-utils.mjs';
|
|
13
13
|
import { normalizeContentForGeminiParts, splitToolContentForGemini } from './media-normalization.mjs';
|
|
14
14
|
|
|
15
|
+
function explicitGeminiMediaPart(part) {
|
|
16
|
+
if (!part || typeof part !== 'object') return null;
|
|
17
|
+
const inline = part.inlineData || part.inline_data;
|
|
18
|
+
if (typeof inline?.data === 'string' && inline.data) {
|
|
19
|
+
const mimeType = inline.mimeType || inline.mime_type || inline.mediaType || inline.media_type;
|
|
20
|
+
if (typeof mimeType === 'string' && mimeType) {
|
|
21
|
+
return { inlineData: { mimeType, data: inline.data } };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const file = part.fileData || part.file_data;
|
|
25
|
+
const fileUri = file?.fileUri || file?.file_uri;
|
|
26
|
+
const mimeType = file?.mimeType || file?.mime_type || file?.mediaType || file?.media_type;
|
|
27
|
+
if (typeof fileUri === 'string' && fileUri && typeof mimeType === 'string' && mimeType) {
|
|
28
|
+
return { fileData: { mimeType, fileUri } };
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The shared normalizer intentionally defaults unknown media to image/png for
|
|
34
|
+
// image-oriented providers. Gemini accepts audio/video/document MIME types too,
|
|
35
|
+
// so preserve explicit Gemini wire parts before using the shared fallback.
|
|
36
|
+
export function normalizeGeminiParts(content) {
|
|
37
|
+
if (typeof content === 'string') return normalizeContentForGeminiParts(content);
|
|
38
|
+
const parts = Array.isArray(content)
|
|
39
|
+
? content
|
|
40
|
+
: (content && typeof content === 'object' && Array.isArray(content.content)
|
|
41
|
+
? content.content
|
|
42
|
+
: [content]);
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const part of parts) {
|
|
45
|
+
const media = explicitGeminiMediaPart(part);
|
|
46
|
+
if (media) out.push(media);
|
|
47
|
+
else if (typeof part === 'string') out.push({ text: part });
|
|
48
|
+
else if (part && typeof part === 'object' && typeof part.text === 'string') out.push({ text: part.text });
|
|
49
|
+
else out.push(...normalizeContentForGeminiParts(part));
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function splitGeminiToolContent(content) {
|
|
55
|
+
const normalized = normalizeGeminiParts(content);
|
|
56
|
+
const explicitMedia = normalized.filter((part) => part?.inlineData || part?.fileData);
|
|
57
|
+
if (!explicitMedia.length) return splitToolContentForGemini(content);
|
|
58
|
+
const text = normalized
|
|
59
|
+
.filter((part) => typeof part?.text === 'string')
|
|
60
|
+
.map((part) => part.text)
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.join('\n');
|
|
63
|
+
return {
|
|
64
|
+
response: { result: text || '[tool result included media content]' },
|
|
65
|
+
mediaParts: explicitMedia,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const GEMINI_FUNCTION_RESPONSE_MIME = /^(?:image\/(?:png|jpeg|webp)|application\/pdf|text\/plain)$/i;
|
|
70
|
+
|
|
71
|
+
function toGeminiFunctionResponseMedia(mediaParts) {
|
|
72
|
+
const parts = [];
|
|
73
|
+
const refs = [];
|
|
74
|
+
const external = [];
|
|
75
|
+
const omitted = [];
|
|
76
|
+
for (const media of mediaParts || []) {
|
|
77
|
+
const payload = media?.inlineData || media?.fileData;
|
|
78
|
+
const mimeType = String(payload?.mimeType || '').trim();
|
|
79
|
+
if (!GEMINI_FUNCTION_RESPONSE_MIME.test(mimeType)) {
|
|
80
|
+
if (mimeType) omitted.push(mimeType);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const displayName = `tool_media_${parts.length + 1}`;
|
|
84
|
+
if (media.inlineData) {
|
|
85
|
+
parts.push({ inlineData: { ...media.inlineData, mimeType, displayName } });
|
|
86
|
+
refs.push({ $ref: displayName });
|
|
87
|
+
} else {
|
|
88
|
+
// FunctionResponse.parts currently documents inlineData only.
|
|
89
|
+
// fileData is valid in ordinary Content parts but can 400 when
|
|
90
|
+
// nested here, so retain file-backed media as plain JSON metadata.
|
|
91
|
+
external.push({
|
|
92
|
+
mimeType,
|
|
93
|
+
fileUri: String(media.fileData?.fileUri || ''),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { parts, refs, external, omitted };
|
|
98
|
+
}
|
|
99
|
+
|
|
15
100
|
/**
|
|
16
101
|
* Convert JSON Schema type string to Gemini SchemaType.
|
|
17
102
|
* Gemini SDK uses its own enum instead of plain strings.
|
|
@@ -31,34 +116,282 @@ function toSchemaType(t) {
|
|
|
31
116
|
/**
|
|
32
117
|
* Recursively convert a JSON Schema object to Gemini's FunctionDeclarationSchema.
|
|
33
118
|
* Gemini requires `type` to be a SchemaType enum, not a plain string, and
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* patternProperties). We strip those at every level.
|
|
119
|
+
* accepts only a documented subset of JSON Schema. Project onto that subset
|
|
120
|
+
* at every level and convert `const` into Gemini's string enum form.
|
|
37
121
|
*/
|
|
38
|
-
const
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
122
|
+
const GEMINI_SCHEMA_FIELDS = new Set([
|
|
123
|
+
'type',
|
|
124
|
+
'format',
|
|
125
|
+
'title',
|
|
126
|
+
'description',
|
|
127
|
+
'nullable',
|
|
128
|
+
'enum',
|
|
129
|
+
'items',
|
|
130
|
+
'properties',
|
|
131
|
+
'required',
|
|
132
|
+
'example',
|
|
133
|
+
'default',
|
|
134
|
+
'anyOf',
|
|
135
|
+
'oneOf',
|
|
136
|
+
'allOf',
|
|
137
|
+
'not',
|
|
138
|
+
'minimum',
|
|
139
|
+
'maximum',
|
|
140
|
+
'exclusiveMinimum',
|
|
141
|
+
'exclusiveMaximum',
|
|
142
|
+
'minItems',
|
|
143
|
+
'maxItems',
|
|
144
|
+
'minProperties',
|
|
145
|
+
'maxProperties',
|
|
146
|
+
'minLength',
|
|
147
|
+
'maxLength',
|
|
148
|
+
'pattern',
|
|
149
|
+
'propertyOrdering',
|
|
42
150
|
'const',
|
|
43
|
-
'examples',
|
|
44
|
-
'definitions',
|
|
45
|
-
'patternProperties',
|
|
46
151
|
]);
|
|
152
|
+
|
|
153
|
+
const SCHEMA_CONFLICT_ENUM = '__mixdog_unrepresentable_schema_conjunction__';
|
|
154
|
+
const schemaValueKey = (value) => stableTraceStringify(value);
|
|
155
|
+
const sameSchemaValue = (a, b) => schemaValueKey(a) === schemaValueKey(b);
|
|
156
|
+
const defineSchemaProperty = (target, name, value) => {
|
|
157
|
+
Object.defineProperty(target, name, {
|
|
158
|
+
value,
|
|
159
|
+
enumerable: true,
|
|
160
|
+
writable: true,
|
|
161
|
+
configurable: true,
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
function schemaFallback(reason) {
|
|
166
|
+
return {
|
|
167
|
+
type: SchemaType.STRING,
|
|
168
|
+
enum: [SCHEMA_CONFLICT_ENUM],
|
|
169
|
+
description: `Schema conjunction could not be represented safely: ${String(reason || 'conflict').slice(0, 300)}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function typeInfo(schema) {
|
|
174
|
+
const declared = Array.isArray(schema?.type)
|
|
175
|
+
? schema.type
|
|
176
|
+
: (typeof schema?.type === 'string' ? [schema.type] : []);
|
|
177
|
+
const nonNull = [...new Set(declared.filter((type) => type !== 'null'))];
|
|
178
|
+
let type = nonNull[0] || null;
|
|
179
|
+
if (nonNull.length > 1) {
|
|
180
|
+
if (nonNull.length === 2 && nonNull.includes('integer') && nonNull.includes('number')) type = 'integer';
|
|
181
|
+
else return { conflict: `unsupported type union ${nonNull.join('|')}` };
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
type,
|
|
185
|
+
allowsNull: declared.length === 0 || declared.includes('null') || schema?.nullable === true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function intersectTypeInfo(a, b) {
|
|
190
|
+
const left = typeInfo(a);
|
|
191
|
+
const right = typeInfo(b);
|
|
192
|
+
if (left.conflict || right.conflict) return { conflict: left.conflict || right.conflict };
|
|
193
|
+
let type = left.type || right.type;
|
|
194
|
+
if (left.type && right.type && left.type !== right.type) {
|
|
195
|
+
if ([left.type, right.type].includes('integer') && [left.type, right.type].includes('number')) type = 'integer';
|
|
196
|
+
else return { conflict: `incompatible types ${left.type} and ${right.type}` };
|
|
197
|
+
}
|
|
198
|
+
return { type, allowsNull: left.allowsNull && right.allowsNull };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function enumValues(schema) {
|
|
202
|
+
const values = Array.isArray(schema?.enum) ? schema.enum : null;
|
|
203
|
+
if (schema?.const === undefined) return { values };
|
|
204
|
+
if (values && !values.some((value) => sameSchemaValue(value, schema.const))) {
|
|
205
|
+
return { values: null, conflict: 'empty enum intersection' };
|
|
206
|
+
}
|
|
207
|
+
return { values: [schema.const] };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function strongerLower(a, b) {
|
|
211
|
+
if (!a) return b;
|
|
212
|
+
if (!b) return a;
|
|
213
|
+
if (b.value > a.value) return b;
|
|
214
|
+
if (b.value < a.value) return a;
|
|
215
|
+
return { value: a.value, exclusive: a.exclusive || b.exclusive };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function strongerUpper(a, b) {
|
|
219
|
+
if (!a) return b;
|
|
220
|
+
if (!b) return a;
|
|
221
|
+
if (b.value < a.value) return b;
|
|
222
|
+
if (b.value > a.value) return a;
|
|
223
|
+
return { value: a.value, exclusive: a.exclusive || b.exclusive };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function rawBound(schema, side) {
|
|
227
|
+
const inclusive = side === 'lower' ? schema?.minimum : schema?.maximum;
|
|
228
|
+
const exclusive = side === 'lower' ? schema?.exclusiveMinimum : schema?.exclusiveMaximum;
|
|
229
|
+
let out = Number.isFinite(Number(inclusive)) ? { value: Number(inclusive), exclusive: false } : null;
|
|
230
|
+
if (Number.isFinite(Number(exclusive))) {
|
|
231
|
+
const next = { value: Number(exclusive), exclusive: true };
|
|
232
|
+
out = side === 'lower' ? strongerLower(out, next) : strongerUpper(out, next);
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function mergeSchemaConjunction(leftInput, rightInput) {
|
|
238
|
+
const left = flattenAllOf(leftInput);
|
|
239
|
+
const right = flattenAllOf(rightInput);
|
|
240
|
+
if (left.conflict || right.conflict) return { conflict: left.conflict || right.conflict };
|
|
241
|
+
for (const key of ['anyOf', 'oneOf', 'not']) {
|
|
242
|
+
if (right.schema[key] !== undefined) return { conflict: `${key} inside allOf` };
|
|
243
|
+
}
|
|
244
|
+
const a = left.schema;
|
|
245
|
+
const b = right.schema;
|
|
246
|
+
for (const source of [a, b]) {
|
|
247
|
+
for (const key of Object.keys(source)) {
|
|
248
|
+
if (GEMINI_SCHEMA_FIELDS.has(key)) continue;
|
|
249
|
+
return { conflict: `unsupported allOf keyword ${key}` };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const merged = { ...a };
|
|
253
|
+
const types = intersectTypeInfo(a, b);
|
|
254
|
+
if (types.conflict) return types;
|
|
255
|
+
delete merged.type;
|
|
256
|
+
delete merged.nullable;
|
|
257
|
+
if (types.type) merged.type = types.type;
|
|
258
|
+
if (types.allowsNull && types.type) merged.nullable = true;
|
|
259
|
+
|
|
260
|
+
const aEnum = enumValues(a);
|
|
261
|
+
const bEnum = enumValues(b);
|
|
262
|
+
if (aEnum.conflict || bEnum.conflict) {
|
|
263
|
+
return { conflict: aEnum.conflict || bEnum.conflict };
|
|
264
|
+
}
|
|
265
|
+
const ae = aEnum.values;
|
|
266
|
+
const be = bEnum.values;
|
|
267
|
+
if (ae || be) {
|
|
268
|
+
const values = ae && be
|
|
269
|
+
? ae.filter((value) => be.some((candidate) => sameSchemaValue(value, candidate)))
|
|
270
|
+
: [...(ae || be)];
|
|
271
|
+
if (!values.length) return { conflict: 'empty enum intersection' };
|
|
272
|
+
merged.enum = values;
|
|
273
|
+
delete merged.const;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const lower = strongerLower(rawBound(a, 'lower'), rawBound(b, 'lower'));
|
|
277
|
+
const upper = strongerUpper(rawBound(a, 'upper'), rawBound(b, 'upper'));
|
|
278
|
+
for (const key of ['minimum', 'exclusiveMinimum', 'maximum', 'exclusiveMaximum']) delete merged[key];
|
|
279
|
+
if (lower) merged[lower.exclusive ? 'exclusiveMinimum' : 'minimum'] = lower.value;
|
|
280
|
+
if (upper) merged[upper.exclusive ? 'exclusiveMaximum' : 'maximum'] = upper.value;
|
|
281
|
+
if (lower && upper && (lower.value > upper.value
|
|
282
|
+
|| (lower.value === upper.value && (lower.exclusive || upper.exclusive)))) {
|
|
283
|
+
return { conflict: 'empty numeric range' };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const [minKey, maxKey] of [
|
|
287
|
+
['minLength', 'maxLength'],
|
|
288
|
+
['minItems', 'maxItems'],
|
|
289
|
+
['minProperties', 'maxProperties'],
|
|
290
|
+
]) {
|
|
291
|
+
const min = Math.max(...[a[minKey], b[minKey]].filter(Number.isFinite), 0);
|
|
292
|
+
const maxima = [a[maxKey], b[maxKey]].filter(Number.isFinite);
|
|
293
|
+
const max = maxima.length ? Math.min(...maxima) : null;
|
|
294
|
+
if (max != null && min > max) return { conflict: `${minKey}/${maxKey}` };
|
|
295
|
+
if (min > 0 || a[minKey] === 0 || b[minKey] === 0) merged[minKey] = min;
|
|
296
|
+
if (max != null) merged[maxKey] = max;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const key of ['pattern', 'format']) {
|
|
300
|
+
if (a[key] !== undefined && b[key] !== undefined && a[key] !== b[key]) {
|
|
301
|
+
return { conflict: `incompatible ${key}` };
|
|
302
|
+
}
|
|
303
|
+
if (b[key] !== undefined) merged[key] = b[key];
|
|
304
|
+
}
|
|
305
|
+
if (a.description && b.description && a.description !== b.description) {
|
|
306
|
+
merged.description = `${a.description}\n${b.description}`;
|
|
307
|
+
} else if (b.description !== undefined) merged.description = b.description;
|
|
308
|
+
if (a.default !== undefined && b.default !== undefined && !sameSchemaValue(a.default, b.default)) {
|
|
309
|
+
delete merged.default;
|
|
310
|
+
} else if (b.default !== undefined) merged.default = b.default;
|
|
311
|
+
for (const key of ['title', 'example', 'propertyOrdering']) {
|
|
312
|
+
if (merged[key] === undefined && b[key] !== undefined) merged[key] = b[key];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const properties = {};
|
|
316
|
+
for (const [name, schema] of Object.entries(a.properties || {})) {
|
|
317
|
+
defineSchemaProperty(properties, name, schema);
|
|
318
|
+
}
|
|
319
|
+
for (const [name, schema] of Object.entries(b.properties || {})) {
|
|
320
|
+
if (!Object.hasOwn(properties, name)) defineSchemaProperty(properties, name, schema);
|
|
321
|
+
else {
|
|
322
|
+
const child = mergeSchemaConjunction(properties[name], schema);
|
|
323
|
+
if (child.conflict) return { conflict: `property ${name}: ${child.conflict}` };
|
|
324
|
+
defineSchemaProperty(properties, name, child.schema);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (Object.keys(properties).length) merged.properties = properties;
|
|
328
|
+
const required = [...new Set([
|
|
329
|
+
...(Array.isArray(a.required) ? a.required : []),
|
|
330
|
+
...(Array.isArray(b.required) ? b.required : []),
|
|
331
|
+
].filter((name) => typeof name === 'string'))];
|
|
332
|
+
if (required.length) merged.required = required;
|
|
333
|
+
|
|
334
|
+
if (a.items && b.items) {
|
|
335
|
+
const items = mergeSchemaConjunction(a.items, b.items);
|
|
336
|
+
if (items.conflict) return { conflict: `items: ${items.conflict}` };
|
|
337
|
+
merged.items = items.schema;
|
|
338
|
+
} else if (b.items) merged.items = b.items;
|
|
339
|
+
return { schema: merged };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function flattenAllOf(input) {
|
|
343
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return { schema: input };
|
|
344
|
+
let schema = { ...input };
|
|
345
|
+
const branches = Array.isArray(schema.allOf) ? schema.allOf : [];
|
|
346
|
+
const hadAllOf = branches.length > 0;
|
|
347
|
+
delete schema.allOf;
|
|
348
|
+
for (const branch of branches) {
|
|
349
|
+
const merged = mergeSchemaConjunction(schema, branch);
|
|
350
|
+
if (merged.conflict) return merged;
|
|
351
|
+
schema = merged.schema;
|
|
352
|
+
}
|
|
353
|
+
return { schema, hadAllOf };
|
|
354
|
+
}
|
|
355
|
+
|
|
47
356
|
export function convertSchema(schema) {
|
|
48
357
|
if (!schema || typeof schema !== 'object') return schema;
|
|
358
|
+
const flattened = flattenAllOf(schema);
|
|
359
|
+
if (flattened.conflict) return schemaFallback(flattened.conflict);
|
|
360
|
+
schema = flattened.schema;
|
|
49
361
|
const result = {};
|
|
50
362
|
for (const [k, v] of Object.entries(schema)) {
|
|
51
|
-
|
|
363
|
+
// Project onto Gemini's documented Schema surface instead of trying
|
|
364
|
+
// to blacklist the much larger JSON Schema vocabulary. Unknown
|
|
365
|
+
// extension keywords are rejected by generateContent.
|
|
366
|
+
if (!GEMINI_SCHEMA_FIELDS.has(k)) continue;
|
|
52
367
|
result[k] = v;
|
|
53
368
|
}
|
|
54
|
-
// Gemini's Schema validator requires every `enum` entry to be a string
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
369
|
+
// Gemini's Schema validator requires every `enum` entry to be a string.
|
|
370
|
+
// An unrepresentable typed enum must become an explicit conflict schema;
|
|
371
|
+
// dropping it would broaden the accepted tool arguments.
|
|
372
|
+
// Gemini's schema dialect represents JSON Schema null unions with
|
|
373
|
+
// `nullable`, not a type array.
|
|
374
|
+
if (result.type === undefined && (result.properties || result.required)) result.type = 'object';
|
|
375
|
+
if (result.type === undefined && result.items) result.type = 'array';
|
|
376
|
+
const normalizedType = typeInfo(result);
|
|
377
|
+
if (normalizedType.conflict) return schemaFallback(normalizedType.conflict);
|
|
378
|
+
const rawType = normalizedType.type || undefined;
|
|
379
|
+
if (rawType) result.type = rawType;
|
|
380
|
+
else delete result.type;
|
|
381
|
+
if (normalizedType.allowsNull && rawType) result.nullable = true;
|
|
382
|
+
else if (result.nullable !== true) delete result.nullable;
|
|
383
|
+
const localEnum = enumValues(result);
|
|
384
|
+
if (localEnum.conflict) return schemaFallback(localEnum.conflict);
|
|
385
|
+
if (result.const !== undefined) {
|
|
386
|
+
if (rawType === 'integer' || rawType === 'number' || rawType === 'boolean') {
|
|
387
|
+
return schemaFallback(`Gemini does not support ${rawType} const`);
|
|
388
|
+
}
|
|
389
|
+
result.enum = [String(result.const)];
|
|
390
|
+
delete result.const;
|
|
391
|
+
}
|
|
59
392
|
if (Array.isArray(result.enum) && (rawType === 'integer' || rawType === 'number' || rawType === 'boolean')) {
|
|
60
393
|
if (result.enum.some((item) => typeof item !== 'string')) {
|
|
61
|
-
|
|
394
|
+
return schemaFallback(`Gemini does not support ${rawType} enum conjunction`);
|
|
62
395
|
}
|
|
63
396
|
}
|
|
64
397
|
// Gemini rejects array schemas that omit `items`; fill a permissive
|
|
@@ -69,20 +402,46 @@ export function convertSchema(schema) {
|
|
|
69
402
|
if (typeof result.type === 'string') {
|
|
70
403
|
result.type = toSchemaType(result.type);
|
|
71
404
|
}
|
|
72
|
-
|
|
405
|
+
const lower = rawBound(result, 'lower');
|
|
406
|
+
const upper = rawBound(result, 'upper');
|
|
407
|
+
delete result.exclusiveMinimum;
|
|
408
|
+
delete result.exclusiveMaximum;
|
|
409
|
+
if ((lower?.exclusive || upper?.exclusive) && rawType !== 'integer') {
|
|
410
|
+
return schemaFallback('exclusive numeric bound is not supported by Gemini Schema');
|
|
411
|
+
}
|
|
412
|
+
if (rawType === 'integer') {
|
|
413
|
+
if (lower?.exclusive) result.minimum = Math.floor(lower.value) + 1;
|
|
414
|
+
if (upper?.exclusive) result.maximum = Math.ceil(upper.value) - 1;
|
|
415
|
+
}
|
|
416
|
+
if (Number.isFinite(result.minimum) && Number.isFinite(result.maximum)
|
|
417
|
+
&& result.minimum > result.maximum) return schemaFallback('empty numeric range');
|
|
418
|
+
if ((result.properties && typeof result.properties === 'object') || Array.isArray(result.required)) {
|
|
73
419
|
const props = {};
|
|
74
|
-
for (const [key, val] of Object.entries(result.properties)) {
|
|
75
|
-
props
|
|
420
|
+
for (const [key, val] of Object.entries(result.properties || {})) {
|
|
421
|
+
defineSchemaProperty(props, key, convertSchema(val));
|
|
422
|
+
}
|
|
423
|
+
if (Array.isArray(result.required)) {
|
|
424
|
+
result.required = [...new Set(result.required.filter((key) => typeof key === 'string'))];
|
|
425
|
+
for (const key of result.required) {
|
|
426
|
+
if (!Object.hasOwn(props, key)) {
|
|
427
|
+
defineSchemaProperty(
|
|
428
|
+
props,
|
|
429
|
+
key,
|
|
430
|
+
schemaFallback(`required property ${key} has no representable schema`),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (!result.required.length) delete result.required;
|
|
76
435
|
}
|
|
77
436
|
result.properties = props;
|
|
78
437
|
}
|
|
79
438
|
if (result.items && typeof result.items === 'object') {
|
|
80
439
|
result.items = convertSchema(result.items);
|
|
81
440
|
}
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
441
|
+
// Gemini function declarations support anyOf, but not oneOf/allOf/not.
|
|
442
|
+
// oneOf is safely relaxed to anyOf. Object allOf branches are projected
|
|
443
|
+
// into the local properties/required set where representable; other allOf
|
|
444
|
+
// and all not constraints are conservatively dropped.
|
|
86
445
|
//
|
|
87
446
|
// Two Gemini-specific normalizations are also applied per combinator
|
|
88
447
|
// subschema:
|
|
@@ -94,9 +453,13 @@ export function convertSchema(schema) {
|
|
|
94
453
|
// validates that every name in `required` exists in *this*
|
|
95
454
|
// subschema's `properties` (it does not inherit from the parent
|
|
96
455
|
// the way JSON Schema's compositional model does).
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
456
|
+
const unionBranches = [
|
|
457
|
+
...(Array.isArray(result.anyOf) ? result.anyOf : []),
|
|
458
|
+
...(Array.isArray(result.oneOf) ? result.oneOf : []),
|
|
459
|
+
];
|
|
460
|
+
delete result.oneOf;
|
|
461
|
+
if (unionBranches.length) {
|
|
462
|
+
result.anyOf = unionBranches.map((s) => {
|
|
100
463
|
const sub = convertSchema(s);
|
|
101
464
|
if (sub && typeof sub === 'object') {
|
|
102
465
|
const usesObjectKeys = sub.required !== undefined || sub.properties !== undefined;
|
|
@@ -106,18 +469,48 @@ export function convertSchema(schema) {
|
|
|
106
469
|
if (Array.isArray(sub.required) && !sub.properties && result.properties) {
|
|
107
470
|
const projected = {};
|
|
108
471
|
for (const k of sub.required) {
|
|
109
|
-
if (result.properties
|
|
472
|
+
if (Object.hasOwn(result.properties, k)) {
|
|
473
|
+
defineSchemaProperty(projected, k, result.properties[k]);
|
|
474
|
+
}
|
|
110
475
|
}
|
|
111
476
|
if (Object.keys(projected).length > 0) sub.properties = projected;
|
|
112
477
|
}
|
|
113
478
|
}
|
|
114
479
|
return sub;
|
|
115
|
-
});
|
|
116
|
-
|
|
480
|
+
}).filter(Boolean);
|
|
481
|
+
if (!result.anyOf.length) delete result.anyOf;
|
|
117
482
|
}
|
|
118
|
-
|
|
119
|
-
|
|
483
|
+
delete result.allOf;
|
|
484
|
+
delete result.not;
|
|
485
|
+
const typeSpecific = {
|
|
486
|
+
string: new Set(['minLength', 'maxLength', 'pattern', 'format']),
|
|
487
|
+
array: new Set(['minItems', 'maxItems', 'items']),
|
|
488
|
+
object: new Set(['minProperties', 'maxProperties', 'properties', 'required', 'propertyOrdering']),
|
|
489
|
+
number: new Set(['minimum', 'maximum']),
|
|
490
|
+
integer: new Set(['minimum', 'maximum']),
|
|
491
|
+
};
|
|
492
|
+
for (const key of [
|
|
493
|
+
'minLength', 'maxLength', 'pattern', 'format',
|
|
494
|
+
'minItems', 'maxItems', 'items',
|
|
495
|
+
'minProperties', 'maxProperties', 'properties', 'required', 'propertyOrdering',
|
|
496
|
+
'minimum', 'maximum',
|
|
497
|
+
]) {
|
|
498
|
+
if (!typeSpecific[rawType]?.has(key)) delete result[key];
|
|
120
499
|
}
|
|
500
|
+
for (const [minKey, maxKey] of [
|
|
501
|
+
['minLength', 'maxLength'],
|
|
502
|
+
['minItems', 'maxItems'],
|
|
503
|
+
['minProperties', 'maxProperties'],
|
|
504
|
+
]) {
|
|
505
|
+
if (result[minKey] !== undefined
|
|
506
|
+
&& (!Number.isInteger(result[minKey]) || result[minKey] < 0)) delete result[minKey];
|
|
507
|
+
if (result[maxKey] !== undefined
|
|
508
|
+
&& (!Number.isInteger(result[maxKey]) || result[maxKey] < 0)) delete result[maxKey];
|
|
509
|
+
if (result[minKey] !== undefined && result[maxKey] !== undefined
|
|
510
|
+
&& result[minKey] > result[maxKey]) return schemaFallback(`${minKey}/${maxKey}`);
|
|
511
|
+
}
|
|
512
|
+
if (result.minimum !== undefined && !Number.isFinite(result.minimum)) delete result.minimum;
|
|
513
|
+
if (result.maximum !== undefined && !Number.isFinite(result.maximum)) delete result.maximum;
|
|
121
514
|
return result;
|
|
122
515
|
}
|
|
123
516
|
|
|
@@ -192,16 +585,27 @@ export function toGeminiToolConfig(toolChoice) {
|
|
|
192
585
|
function toGeminiContent(message, toolNameByCallId) {
|
|
193
586
|
if (!message || message.role === 'system') return null;
|
|
194
587
|
if (message.role === 'assistant' && message.toolCalls?.length) {
|
|
195
|
-
const parts =
|
|
196
|
-
if (message.content)
|
|
588
|
+
const parts = geminiThoughtPartsFromMetadata(message);
|
|
589
|
+
if (message.content) {
|
|
590
|
+
parts.push(...(geminiTextPartsFromMetadata(message) || normalizeGeminiParts(message.content)));
|
|
591
|
+
}
|
|
197
592
|
for (const tc of message.toolCalls) {
|
|
198
593
|
// Gemini 3 thinking models require the original thoughtSignature
|
|
199
594
|
// echoed back on every prior functionCall so the cached thinking
|
|
200
595
|
// prefix stays valid. v1beta places the field at the Part level
|
|
201
596
|
// (sibling of functionCall) — putting it inside functionCall returns
|
|
202
597
|
// 400 "Unknown name". Older models / first turn have no signature.
|
|
203
|
-
const part = {
|
|
204
|
-
|
|
598
|
+
const part = {
|
|
599
|
+
functionCall: {
|
|
600
|
+
name: tc.name,
|
|
601
|
+
args: tc.arguments,
|
|
602
|
+
...(typeof tc.id === 'string' && tc.id ? { id: tc.id } : {}),
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
if (typeof tc.thoughtSignature === 'string'
|
|
606
|
+
&& tc.thoughtSignature && tc.thoughtSignature.length <= 16_384) {
|
|
607
|
+
part.thoughtSignature = tc.thoughtSignature;
|
|
608
|
+
}
|
|
205
609
|
parts.push(part);
|
|
206
610
|
}
|
|
207
611
|
return { role: 'model', parts };
|
|
@@ -219,9 +623,19 @@ function toGeminiContent(message, toolNameByCallId) {
|
|
|
219
623
|
const functionName = (toolNameByCallId && toolNameByCallId.get(message.toolCallId))
|
|
220
624
|
|| message.toolCallId
|
|
221
625
|
|| '';
|
|
222
|
-
const { response, mediaParts } =
|
|
223
|
-
const
|
|
224
|
-
if (
|
|
626
|
+
const { response, mediaParts } = splitGeminiToolContent(message.content);
|
|
627
|
+
const media = toGeminiFunctionResponseMedia(mediaParts);
|
|
628
|
+
if (media.refs.length) response.media = media.refs;
|
|
629
|
+
if (media.external.length) response.externalMedia = media.external;
|
|
630
|
+
if (media.omitted.length) {
|
|
631
|
+
response.omittedMediaTypes = media.omitted;
|
|
632
|
+
}
|
|
633
|
+
const functionResponse = { name: functionName, response };
|
|
634
|
+
if (typeof message.toolCallId === 'string' && message.toolCallId) {
|
|
635
|
+
functionResponse.id = message.toolCallId;
|
|
636
|
+
}
|
|
637
|
+
if (media.parts.length) functionResponse.parts = media.parts;
|
|
638
|
+
const parts = [{ functionResponse }];
|
|
225
639
|
return {
|
|
226
640
|
role: 'user',
|
|
227
641
|
parts,
|
|
@@ -229,10 +643,52 @@ function toGeminiContent(message, toolNameByCallId) {
|
|
|
229
643
|
}
|
|
230
644
|
return {
|
|
231
645
|
role: message.role === 'assistant' ? 'model' : 'user',
|
|
232
|
-
parts:
|
|
646
|
+
parts: message.role === 'assistant'
|
|
647
|
+
? (geminiTextPartsFromMetadata(message) || normalizeGeminiParts(message.content))
|
|
648
|
+
: normalizeGeminiParts(message.content),
|
|
233
649
|
};
|
|
234
650
|
}
|
|
235
651
|
|
|
652
|
+
function geminiTextPartsFromMetadata(message) {
|
|
653
|
+
const parts = message?.providerMetadata?.gemini?.textParts;
|
|
654
|
+
if (!Array.isArray(parts) || !parts.length || parts.length > 128) return null;
|
|
655
|
+
const normalized = [];
|
|
656
|
+
for (const part of parts) {
|
|
657
|
+
if (!part || typeof part !== 'object'
|
|
658
|
+
|| typeof part.text !== 'string' || part.text.length > 1_000_000) return null;
|
|
659
|
+
const next = { text: part.text };
|
|
660
|
+
if (part.thoughtSignature !== undefined) {
|
|
661
|
+
if (typeof part.thoughtSignature !== 'string'
|
|
662
|
+
|| !part.thoughtSignature || part.thoughtSignature.length > 16_384) return null;
|
|
663
|
+
next.thoughtSignature = part.thoughtSignature;
|
|
664
|
+
}
|
|
665
|
+
normalized.push(next);
|
|
666
|
+
}
|
|
667
|
+
const expected = typeof message.content === 'string' ? message.content : '';
|
|
668
|
+
return normalized.map((part) => part.text).join('') === expected ? normalized : null;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function geminiThoughtPartsFromMetadata(message) {
|
|
672
|
+
const parts = message?.providerMetadata?.gemini?.thoughtParts;
|
|
673
|
+
if (!Array.isArray(parts) || !parts.length || parts.length > 128) return [];
|
|
674
|
+
const normalized = [];
|
|
675
|
+
for (const part of parts) {
|
|
676
|
+
// Hidden prompt content is replayable only when every persisted part is
|
|
677
|
+
// structurally valid and signed by Gemini. Reject the whole collection
|
|
678
|
+
// on malformed/unsigned data rather than partially accepting metadata.
|
|
679
|
+
if (!part || typeof part !== 'object'
|
|
680
|
+
|| typeof part.text !== 'string' || part.text.length > 1_000_000
|
|
681
|
+
|| typeof part.thoughtSignature !== 'string' || !part.thoughtSignature
|
|
682
|
+
|| part.thoughtSignature.length > 16_384) return [];
|
|
683
|
+
normalized.push({
|
|
684
|
+
text: part.text,
|
|
685
|
+
thought: true,
|
|
686
|
+
thoughtSignature: part.thoughtSignature,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
return normalized;
|
|
690
|
+
}
|
|
691
|
+
|
|
236
692
|
export function toGeminiContents(messages) {
|
|
237
693
|
const contents = [];
|
|
238
694
|
// Map synthetic toolCallId -> function name from prior assistant
|
|
@@ -252,6 +708,57 @@ export function toGeminiContents(messages) {
|
|
|
252
708
|
return contents;
|
|
253
709
|
}
|
|
254
710
|
|
|
711
|
+
export function parseGeminiThinkingParts(parts) {
|
|
712
|
+
if (!Array.isArray(parts)) return undefined;
|
|
713
|
+
const blocks = [];
|
|
714
|
+
for (const part of parts) {
|
|
715
|
+
if (part?.thought !== true) continue;
|
|
716
|
+
const block = {
|
|
717
|
+
type: 'thinking',
|
|
718
|
+
thinking: typeof part.text === 'string' ? part.text : '',
|
|
719
|
+
};
|
|
720
|
+
const signature = part.thoughtSignature || part.thought_signature;
|
|
721
|
+
if (typeof signature === 'string' && signature && signature.length <= 16_384) {
|
|
722
|
+
block.signature = signature;
|
|
723
|
+
}
|
|
724
|
+
blocks.push(block);
|
|
725
|
+
}
|
|
726
|
+
return blocks.length ? blocks : undefined;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function parseGeminiTextPartMetadata(parts) {
|
|
730
|
+
if (!Array.isArray(parts)) return undefined;
|
|
731
|
+
const thoughtParts = parts
|
|
732
|
+
.filter((part) => part?.thought === true && typeof part?.text === 'string')
|
|
733
|
+
.map((part) => {
|
|
734
|
+
const out = { text: part.text };
|
|
735
|
+
const signature = part.thoughtSignature || part.thought_signature;
|
|
736
|
+
if (typeof signature === 'string' && signature && signature.length <= 16_384) {
|
|
737
|
+
out.thoughtSignature = signature;
|
|
738
|
+
}
|
|
739
|
+
return out;
|
|
740
|
+
})
|
|
741
|
+
// Unsigned reasoning must never become persisted hidden prompt text.
|
|
742
|
+
.filter((part) => typeof part.thoughtSignature === 'string' && part.thoughtSignature);
|
|
743
|
+
const textParts = parts
|
|
744
|
+
.filter((part) => part?.thought !== true && typeof part?.text === 'string')
|
|
745
|
+
.map((part) => {
|
|
746
|
+
const out = { text: part.text };
|
|
747
|
+
const signature = part.thoughtSignature || part.thought_signature;
|
|
748
|
+
if (typeof signature === 'string' && signature && signature.length <= 16_384) {
|
|
749
|
+
out.thoughtSignature = signature;
|
|
750
|
+
}
|
|
751
|
+
return out;
|
|
752
|
+
});
|
|
753
|
+
if (!thoughtParts.length && !textParts.some((part) => part.thoughtSignature)) return undefined;
|
|
754
|
+
return {
|
|
755
|
+
gemini: {
|
|
756
|
+
...(thoughtParts.length ? { thoughtParts } : {}),
|
|
757
|
+
...(textParts.some((part) => part.thoughtSignature) ? { textParts } : {}),
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
255
762
|
export function parseToolCalls(parts) {
|
|
256
763
|
const calls = parts.filter((p) => 'functionCall' in p && !!p.functionCall);
|
|
257
764
|
if (!calls.length)
|
|
@@ -282,11 +789,16 @@ export function parseToolCalls(parts) {
|
|
|
282
789
|
args: fc.args ?? {},
|
|
283
790
|
})).slice(0, 16);
|
|
284
791
|
const call = {
|
|
285
|
-
id
|
|
792
|
+
// Gemini may provide an opaque server call id. It is a protocol
|
|
793
|
+
// correlation token: preserve it byte-for-byte and synthesize a
|
|
794
|
+
// deterministic id only for older responses that omit it.
|
|
795
|
+
id: typeof fc.id === 'string' && fc.id ? fc.id : `gemini_${idHash}`,
|
|
286
796
|
name: fc.name,
|
|
287
797
|
arguments: (fc.args ?? {}),
|
|
288
798
|
};
|
|
289
|
-
if (sig
|
|
799
|
+
if (typeof sig === 'string' && sig && sig.length <= 16_384) {
|
|
800
|
+
call.thoughtSignature = sig;
|
|
801
|
+
}
|
|
290
802
|
return call;
|
|
291
803
|
});
|
|
292
804
|
}
|