@demicodes/provider-google 0.8.0 → 0.9.1

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.
Files changed (2) hide show
  1. package/dist/index.mjs +95 -15
  2. package/package.json +4 -4
package/dist/index.mjs CHANGED
@@ -107,6 +107,13 @@ function positiveInteger(value, field) {
107
107
  //#region src/provider.ts
108
108
  const DEFAULT_GOOGLE_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
109
109
  const DEFAULT_MAX_OUTPUT_TOKENS = 32e3;
110
+ /**
111
+ * Marks a thought signature as this provider's own. Transcripts outlive a
112
+ * provider choice — a conversation can start on one and continue on another —
113
+ * and each provider's signature format is private to it, so an untagged (or
114
+ * foreign-tagged) signature must never be replayed as if it were ours.
115
+ */
116
+ const SIGNATURE_TAG = "google:";
110
117
  const DEFAULT_EFFORT_BUDGET_TOKENS = {
111
118
  low: 4096,
112
119
  medium: 16384,
@@ -260,6 +267,7 @@ function googleThinkingConfig(thinking, options) {
260
267
  function inferenceItemsToGoogleContents(items) {
261
268
  const contents = [];
262
269
  const toolNames = /* @__PURE__ */ new Map();
270
+ const degraded = /* @__PURE__ */ new Set();
263
271
  let pendingSignature = null;
264
272
  const append = (role, parts) => {
265
273
  if (parts.length === 0) return;
@@ -284,25 +292,33 @@ function inferenceItemsToGoogleContents(items) {
284
292
  pendingSignature = null;
285
293
  break;
286
294
  case "assistant_thinking":
287
- pendingSignature = item.signature;
295
+ pendingSignature = ownSignature(item.signature);
288
296
  break;
289
297
  case "assistant_redacted_thinking": break;
290
- case "tool_use": {
298
+ case "tool_use":
291
299
  toolNames.set(item.toolUseId, item.toolName);
292
- const call = { functionCall: {
293
- name: item.toolName,
294
- args: item.input ?? {},
295
- id: item.toolUseId
296
- } };
297
- if (pendingSignature) call.thoughtSignature = pendingSignature;
298
- append("model", [call]);
300
+ if (!pendingSignature) {
301
+ degraded.add(item.toolUseId);
302
+ append("model", [{ text: `[called ${item.toolName} with ${stringifyToolInput(item.input)}]` }]);
303
+ break;
304
+ }
305
+ append("model", [{
306
+ functionCall: {
307
+ name: item.toolName,
308
+ args: item.input ?? {},
309
+ id: item.toolUseId
310
+ },
311
+ thoughtSignature: pendingSignature
312
+ }]);
299
313
  pendingSignature = null;
300
314
  break;
301
- }
302
- case "tool_result":
303
- append("user", toolResultToGoogle(item.toolUseId, toolNames.get(item.toolUseId) ?? "tool", item.output));
315
+ case "tool_result": {
316
+ const name = toolNames.get(item.toolUseId) ?? "tool";
317
+ if (degraded.has(item.toolUseId)) append("user", [{ text: `[${name} returned] ${toolResultText(item.output)}` }]);
318
+ else append("user", toolResultToGoogle(item.toolUseId, name, item.output));
304
319
  pendingSignature = null;
305
320
  break;
321
+ }
306
322
  }
307
323
  return contents;
308
324
  }
@@ -339,11 +355,75 @@ function toolResultToGoogle(toolUseId, toolName, output) {
339
355
  response: { output: text }
340
356
  } }, ...media];
341
357
  }
358
+ /**
359
+ * Keywords Gemini's function-declaration schema accepts. Its `parameters` is an
360
+ * OpenAPI 3.0 subset, not JSON Schema, and it rejects the whole request on the
361
+ * first keyword it does not know rather than ignoring it:
362
+ *
363
+ * Invalid JSON payload received. Unknown name "additionalProperties"
364
+ * at 'tools[0].function_declarations[3].parameters': Cannot find field.
365
+ *
366
+ * `additionalProperties: false` is exactly what a careful tool author writes —
367
+ * demi's own shell tools all do — so passing schemas through verbatim breaks
368
+ * every agent that uses them.
369
+ */
370
+ const GOOGLE_SCHEMA_KEYS = /* @__PURE__ */ new Set([
371
+ "type",
372
+ "format",
373
+ "title",
374
+ "description",
375
+ "nullable",
376
+ "enum",
377
+ "items",
378
+ "properties",
379
+ "required",
380
+ "minimum",
381
+ "maximum",
382
+ "minItems",
383
+ "maxItems",
384
+ "anyOf",
385
+ "default"
386
+ ]);
387
+ /**
388
+ * Drops keywords Gemini does not accept, recursing through the containers that
389
+ * hold nested schemas. Dropping is the right failure mode here: the discarded
390
+ * keywords ($schema, additionalProperties, allOf…) constrain what the model may
391
+ * send, and a slightly loose tool schema still validates on demi's side, where
392
+ * the command parses its own input anyway. Rejecting or erroring would break
393
+ * callers over a constraint the transport merely cannot express.
394
+ */
395
+ function toGoogleSchema(schema) {
396
+ if (!isRecord(schema)) return {};
397
+ const out = {};
398
+ for (const [key, value] of Object.entries(schema)) {
399
+ if (!GOOGLE_SCHEMA_KEYS.has(key)) continue;
400
+ if (key === "properties" && isRecord(value)) out.properties = Object.fromEntries(Object.entries(value).map(([name, child]) => [name, toGoogleSchema(child)]));
401
+ else if (key === "items") out.items = toGoogleSchema(value);
402
+ else if (key === "anyOf" && Array.isArray(value)) out.anyOf = value.map(toGoogleSchema);
403
+ else out[key] = value;
404
+ }
405
+ return out;
406
+ }
407
+ /** Unwraps a signature this provider issued; anything else is not ours to replay. */
408
+ function ownSignature(signature) {
409
+ if (!signature || !signature.startsWith(SIGNATURE_TAG)) return null;
410
+ return signature.slice(7) || null;
411
+ }
412
+ function stringifyToolInput(input) {
413
+ try {
414
+ return JSON.stringify(input ?? {}) ?? "{}";
415
+ } catch {
416
+ return "{}";
417
+ }
418
+ }
419
+ function toolResultText(output) {
420
+ return output.map((block) => block.type === "text" ? block.text : `[${block.source.mediaType}]`).join("\n");
421
+ }
342
422
  function toolToGoogleFunctionDeclaration(tool) {
343
423
  return {
344
424
  name: tool.name,
345
425
  description: tool.description,
346
- parameters: tool.inputSchema
426
+ parameters: toGoogleSchema(tool.inputSchema)
347
427
  };
348
428
  }
349
429
  async function* mapGoogleContentStream(events, signal) {
@@ -382,7 +462,7 @@ async function* mapGoogleContentStream(events, signal) {
382
462
  thinkingOpen = true;
383
463
  yield {
384
464
  type: "thinking_signature",
385
- signature
465
+ signature: `${SIGNATURE_TAG}${signature}`
386
466
  };
387
467
  }
388
468
  yield {
@@ -409,7 +489,7 @@ async function* mapGoogleContentStream(events, signal) {
409
489
  const signature = stringOrNull(part.thoughtSignature);
410
490
  if (signature && thinkingOpen) yield {
411
491
  type: "thinking_signature",
412
- signature
492
+ signature: `${SIGNATURE_TAG}${signature}`
413
493
  };
414
494
  if (text) {
415
495
  thinkingOpen = false;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@demicodes/provider-google",
3
3
  "description": "Google Gemini API provider adapter for Demi.",
4
- "version": "0.8.0",
4
+ "version": "0.9.1",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -11,9 +11,9 @@
11
11
  }
12
12
  },
13
13
  "dependencies": {
14
- "@demicodes/core": "^0.8.0",
15
- "@demicodes/provider": "^0.8.0",
16
- "@demicodes/utils": "^0.8.0"
14
+ "@demicodes/core": "^0.9.1",
15
+ "@demicodes/provider": "^0.9.1",
16
+ "@demicodes/utils": "^0.9.1"
17
17
  },
18
18
  "license": "Apache-2.0",
19
19
  "main": "./dist/index.mjs",