@combycode/llm-sdk 2.2.0 → 2.2.2

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/CHANGELOG.md CHANGED
@@ -4,6 +4,54 @@ All notable changes to `@combycode/llm-sdk` are documented here. The format foll
4
4
  [Keep a Changelog](https://keepachangelog.com/) and the project adheres to
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [2.2.2] — 2026-08-17
8
+
9
+ ### Fixed
10
+
11
+ - **Every Gemini model rejected any tool schema carrying `additionalProperties`.** Function
12
+ declarations were sent on Gemini's `parameters` field, which takes a narrow OpenAPI subset and
13
+ rejects anything outside it outright:
14
+
15
+ > `Invalid JSON payload received. Unknown name "additionalProperties" at 'tools[0].function_declarations[0].parameters'`
16
+
17
+ Since OpenAI strict mode *requires* `additionalProperties: false`, no single tool manifest could
18
+ satisfy both providers — a consuming app had to delete the property, degrading its OpenAI schema,
19
+ to keep Google working. Measured against the live API, the narrow field also rejects `$schema`,
20
+ `$ref`, `$defs`, `definitions`, `const`, `examples`, `exclusiveMinimum`/`Maximum`, `multipleOf`,
21
+ `uniqueItems`, `patternProperties`, `propertyNames`, `if`/`then`, `readOnly`, `deprecated`, and
22
+ `type` given as an array.
23
+
24
+ Schemas now go on `parametersJsonSchema`, which takes full JSON Schema unchanged. Sanitising into
25
+ the subset was the obvious alternative and is worse: it silently drops constraints and cannot
26
+ express a `$ref` at all. Verified end to end on every tool-capable model the catalog ships —
27
+ gemini 2.5 pro/flash/flash-lite, 3-flash, 3.1-pro (incl. customtools), 3.1-flash-lite,
28
+ 3.5-flash/-lite, 3.6-flash, gemma-4-26b/-31b: 11/11 fail before the change and drive the tool
29
+ after it. The Interactions surface already accepted full JSON Schema and is unchanged.
30
+
31
+ ## [2.2.1] — 2026-08-17
32
+
33
+ ### Fixed
34
+
35
+ - **Extended thinking returned a 400 on Claude 4.7 and later.** The Anthropic adapter sent
36
+ `thinking: {type:'enabled', budget_tokens: N}` to every model, on the reasoning that it was the
37
+ universally accepted shape — true when written, and since reversed. Anthropic removed
38
+ `budget_tokens` on 4.7+, so Sonnet 5, Opus 5/4.8/4.7 and Fable 5 rejected every thinking request
39
+ outright:
40
+
41
+ > `"thinking.type.enabled" is not supported for this model. Use "thinking.type.adaptive"`
42
+
43
+ There is no shape that works everywhere: Haiku 4.5, Sonnet 4.5 and the Opus 4.x line have no
44
+ adaptive mode at all and still require the budget (`adaptive thinking is not supported on this
45
+ model`), so a blanket switch would have broken the other half. The shape is now chosen per model
46
+ at 4.6 — the version that accepts both — with `effort` mapping to `output_config.effort` on the
47
+ adaptive side instead of a token budget. An unrecognised model id gets `adaptive`, since
48
+ `budget_tokens` is the shape being retired. Both halves verified against the live API.
49
+
50
+ - **`complete()` silently dropped `thinking`.** The one-shot helper never declared the option, so a
51
+ reasoning request through the simplest entry point sent no thinking at all while `client.complete()`
52
+ and agents honoured it. Found while live-testing the fix above — the run came back green because
53
+ nothing was being sent.
54
+
7
55
  ## [2.2.0] — 2026-08-17
8
56
 
9
57
  ### Added
@@ -15,7 +15,7 @@
15
15
  * LLMClient.complete. Either way the helper destroys its created client
16
16
  * before returning so callers don't leak. */
17
17
  import type { AgentTool } from '../agent/types';
18
- import type { CacheConfig } from '../llm/types/request';
18
+ import type { CacheConfig, ThinkingConfig } from '../llm/types/request';
19
19
  import type { LLMClientConfig } from '../llm/client-config';
20
20
  import type { AudioOptions } from '../llm/types/audio';
21
21
  import type { ContentPart, Message } from '../llm/types/messages';
@@ -67,6 +67,10 @@ export interface CompleteOptions {
67
67
  /** Which output modalities to return. Default ['text']; add 'audio' for a spoken
68
68
  * reply (surfaced as a media part on `response.media`). */
69
69
  outputModalities?: Array<'text' | 'audio'>;
70
+ /** Extended thinking. Missing from this helper until 2.2.1: `client.complete()` and
71
+ * agents honoured `thinking` while a one-shot silently dropped it, so the simplest
72
+ * entry point was the only one that could not reason. */
73
+ thinking?: ThinkingConfig;
70
74
  /** Service tier for this call. Also settable as a `model:tier` suffix (e.g.
71
75
  * `anthropic/claude-opus-4.8:priority`); an explicit value here wins. */
72
76
  serviceTier?: ServiceTier;
@@ -26096,6 +26096,19 @@ var ANTHROPIC_THINKING_BUDGETS = {
26096
26096
  max: 16384
26097
26097
  };
26098
26098
  var DEFAULT_ANTHROPIC_THINKING_BUDGET = 2048;
26099
+ var ANTHROPIC_ADAPTIVE_THINKING_MIN = { major: 4, minor: 6 };
26100
+ function anthropicThinkingShape(model) {
26101
+ const id = model.toLowerCase().replace(/^anthropic\//, "");
26102
+ const modern = /^claude-[a-z]+-(\d+)(?:[-.](\d+))?/.exec(id);
26103
+ if (modern) {
26104
+ const major = Number(modern[1]);
26105
+ const minor = modern[2] === void 0 ? 0 : Number(modern[2]);
26106
+ const { major: minMajor, minor: minMinor } = ANTHROPIC_ADAPTIVE_THINKING_MIN;
26107
+ return major > minMajor || major === minMajor && minor >= minMinor ? "adaptive" : "budgeted";
26108
+ }
26109
+ if (/^claude-\d/.test(id)) return "budgeted";
26110
+ return "adaptive";
26111
+ }
26099
26112
  var ANTHROPIC_TOP_K_MODELS = /^claude-(opus-4-(1|5|6)|sonnet-4-(5|6)|haiku-4-5)(\b|-)/;
26100
26113
  function anthropicAcceptsTopK(model) {
26101
26114
  return ANTHROPIC_TOP_K_MODELS.test(model);
@@ -26477,6 +26490,16 @@ var AnthropicAdapter = class {
26477
26490
  }
26478
26491
  if (req.thinking) {
26479
26492
  if (req.thinking.mode === "off") {
26493
+ } else if (anthropicThinkingShape(req.model) === "adaptive") {
26494
+ const thinking = { type: "adaptive" };
26495
+ if (req.thinking.visibility === "hidden") thinking.display = "omitted";
26496
+ body.thinking = thinking;
26497
+ if (req.thinking.effort) {
26498
+ body.output_config = {
26499
+ ...body.output_config ?? {},
26500
+ effort: req.thinking.effort
26501
+ };
26502
+ }
26480
26503
  } else {
26481
26504
  const budget = req.thinking.effort ? ANTHROPIC_THINKING_BUDGETS[req.thinking.effort] ?? DEFAULT_ANTHROPIC_THINKING_BUDGET : DEFAULT_ANTHROPIC_THINKING_BUDGET;
26482
26505
  const thinking = { type: "enabled", budget_tokens: budget };
@@ -27080,7 +27103,30 @@ var GoogleAdapter = class {
27080
27103
  functionDeclarations: fnTools.map((t) => ({
27081
27104
  name: t.name,
27082
27105
  description: t.description,
27083
- parameters: t.parameters
27106
+ // `parametersJsonSchema`, NOT `parameters`. The two are mutually exclusive
27107
+ // (sending both is a 400) and accept different things:
27108
+ //
27109
+ // parameters a narrow OpenAPI subset. Anything outside it is
27110
+ // rejected outright with `Unknown name "<keyword>"` —
27111
+ // measured: additionalProperties (at any depth),
27112
+ // $schema, $ref, $defs, definitions, const, examples,
27113
+ // exclusiveMinimum/Maximum, multipleOf, uniqueItems,
27114
+ // patternProperties, propertyNames, if/then,
27115
+ // readOnly, deprecated, and `type` as an array.
27116
+ // parametersJsonSchema full JSON Schema.
27117
+ //
27118
+ // We passed callers' schemas straight into `parameters`, so any tool defined
27119
+ // with `additionalProperties: false` — which OpenAI's strict mode requires —
27120
+ // failed on EVERY Gemini model. A consuming app had to delete it from its
27121
+ // manifest, degrading its OpenAI schema to keep Google working.
27122
+ //
27123
+ // Sanitising into the subset was the obvious fix and is the wrong one: it
27124
+ // silently drops constraints and cannot express a `$ref` at all. Verified on
27125
+ // every tool-capable model we ship (2.5 pro/flash/flash-lite, 3-flash,
27126
+ // 3.1-pro incl. customtools, 3.1-flash-lite, 3.5-flash/-lite, 3.6-flash,
27127
+ // gemma-4-26b/-31b) that this field takes the schema unchanged and the model
27128
+ // still calls the tool.
27129
+ parametersJsonSchema: t.parameters
27084
27130
  }))
27085
27131
  });
27086
27132
  }
@@ -38859,7 +38905,8 @@ async function complete(opts) {
38859
38905
  serviceTier,
38860
38906
  cache: opts.cache,
38861
38907
  topK: opts.topK,
38862
- seed: opts.seed
38908
+ seed: opts.seed,
38909
+ thinking: opts.thinking
38863
38910
  });
38864
38911
  } else {
38865
38912
  res = await llm.complete(input, {
@@ -38873,7 +38920,8 @@ async function complete(opts) {
38873
38920
  serviceTier,
38874
38921
  cache: opts.cache,
38875
38922
  topK: opts.topK,
38876
- seed: opts.seed
38923
+ seed: opts.seed,
38924
+ thinking: opts.thinking
38877
38925
  });
38878
38926
  }
38879
38927
  const result = {
package/dist/index.js CHANGED
@@ -26023,6 +26023,19 @@ var ANTHROPIC_THINKING_BUDGETS = {
26023
26023
  max: 16384
26024
26024
  };
26025
26025
  var DEFAULT_ANTHROPIC_THINKING_BUDGET = 2048;
26026
+ var ANTHROPIC_ADAPTIVE_THINKING_MIN = { major: 4, minor: 6 };
26027
+ function anthropicThinkingShape(model) {
26028
+ const id = model.toLowerCase().replace(/^anthropic\//, "");
26029
+ const modern = /^claude-[a-z]+-(\d+)(?:[-.](\d+))?/.exec(id);
26030
+ if (modern) {
26031
+ const major = Number(modern[1]);
26032
+ const minor = modern[2] === void 0 ? 0 : Number(modern[2]);
26033
+ const { major: minMajor, minor: minMinor } = ANTHROPIC_ADAPTIVE_THINKING_MIN;
26034
+ return major > minMajor || major === minMajor && minor >= minMinor ? "adaptive" : "budgeted";
26035
+ }
26036
+ if (/^claude-\d/.test(id)) return "budgeted";
26037
+ return "adaptive";
26038
+ }
26026
26039
  var ANTHROPIC_TOP_K_MODELS = /^claude-(opus-4-(1|5|6)|sonnet-4-(5|6)|haiku-4-5)(\b|-)/;
26027
26040
  function anthropicAcceptsTopK(model) {
26028
26041
  return ANTHROPIC_TOP_K_MODELS.test(model);
@@ -26404,6 +26417,16 @@ var AnthropicAdapter = class {
26404
26417
  }
26405
26418
  if (req.thinking) {
26406
26419
  if (req.thinking.mode === "off") {
26420
+ } else if (anthropicThinkingShape(req.model) === "adaptive") {
26421
+ const thinking = { type: "adaptive" };
26422
+ if (req.thinking.visibility === "hidden") thinking.display = "omitted";
26423
+ body.thinking = thinking;
26424
+ if (req.thinking.effort) {
26425
+ body.output_config = {
26426
+ ...body.output_config ?? {},
26427
+ effort: req.thinking.effort
26428
+ };
26429
+ }
26407
26430
  } else {
26408
26431
  const budget = req.thinking.effort ? ANTHROPIC_THINKING_BUDGETS[req.thinking.effort] ?? DEFAULT_ANTHROPIC_THINKING_BUDGET : DEFAULT_ANTHROPIC_THINKING_BUDGET;
26409
26432
  const thinking = { type: "enabled", budget_tokens: budget };
@@ -27007,7 +27030,30 @@ var GoogleAdapter = class {
27007
27030
  functionDeclarations: fnTools.map((t) => ({
27008
27031
  name: t.name,
27009
27032
  description: t.description,
27010
- parameters: t.parameters
27033
+ // `parametersJsonSchema`, NOT `parameters`. The two are mutually exclusive
27034
+ // (sending both is a 400) and accept different things:
27035
+ //
27036
+ // parameters a narrow OpenAPI subset. Anything outside it is
27037
+ // rejected outright with `Unknown name "<keyword>"` —
27038
+ // measured: additionalProperties (at any depth),
27039
+ // $schema, $ref, $defs, definitions, const, examples,
27040
+ // exclusiveMinimum/Maximum, multipleOf, uniqueItems,
27041
+ // patternProperties, propertyNames, if/then,
27042
+ // readOnly, deprecated, and `type` as an array.
27043
+ // parametersJsonSchema full JSON Schema.
27044
+ //
27045
+ // We passed callers' schemas straight into `parameters`, so any tool defined
27046
+ // with `additionalProperties: false` — which OpenAI's strict mode requires —
27047
+ // failed on EVERY Gemini model. A consuming app had to delete it from its
27048
+ // manifest, degrading its OpenAI schema to keep Google working.
27049
+ //
27050
+ // Sanitising into the subset was the obvious fix and is the wrong one: it
27051
+ // silently drops constraints and cannot express a `$ref` at all. Verified on
27052
+ // every tool-capable model we ship (2.5 pro/flash/flash-lite, 3-flash,
27053
+ // 3.1-pro incl. customtools, 3.1-flash-lite, 3.5-flash/-lite, 3.6-flash,
27054
+ // gemma-4-26b/-31b) that this field takes the schema unchanged and the model
27055
+ // still calls the tool.
27056
+ parametersJsonSchema: t.parameters
27011
27057
  }))
27012
27058
  });
27013
27059
  }
@@ -38786,7 +38832,8 @@ async function complete(opts) {
38786
38832
  serviceTier,
38787
38833
  cache: opts.cache,
38788
38834
  topK: opts.topK,
38789
- seed: opts.seed
38835
+ seed: opts.seed,
38836
+ thinking: opts.thinking
38790
38837
  });
38791
38838
  } else {
38792
38839
  res = await llm.complete(input, {
@@ -38800,7 +38847,8 @@ async function complete(opts) {
38800
38847
  serviceTier,
38801
38848
  cache: opts.cache,
38802
38849
  topK: opts.topK,
38803
- seed: opts.seed
38850
+ seed: opts.seed,
38851
+ thinking: opts.thinking
38804
38852
  });
38805
38853
  }
38806
38854
  const result = {
@@ -11,5 +11,35 @@ export declare const ANTHROPIC_THINKING_BUDGETS: Record<string, number>;
11
11
  * unrecognised.
12
12
  */
13
13
  export declare const DEFAULT_ANTHROPIC_THINKING_BUDGET = 2048;
14
+ /**
15
+ * The version at which `thinking: {type:'adaptive'}` takes over from
16
+ * `{type:'enabled', budget_tokens}`.
17
+ *
18
+ * There is no shape that works everywhere, and the direction reversed under us. This
19
+ * adapter used to send the budgeted form to every model, on the reasoning that it was the
20
+ * universally accepted one — true when it was written. Anthropic then REMOVED
21
+ * `budget_tokens` on 4.7 and later: Sonnet 5, Opus 5/4.8/4.7 and Fable 5 reject it with a
22
+ * 400 ("thinking.type.enabled is not supported for this model"). Meanwhile the older half
23
+ * — Haiku 4.5, Sonnet 4.5, Opus 4.x — has no `adaptive` at all and still requires the
24
+ * budget. So the shape must be chosen per model.
25
+ *
26
+ * 4.6 is the boundary: it accepts both and prefers `adaptive`, everything above requires
27
+ * `adaptive`, everything below requires the budget.
28
+ */
29
+ export declare const ANTHROPIC_ADAPTIVE_THINKING_MIN: {
30
+ readonly major: 4;
31
+ readonly minor: 6;
32
+ };
33
+ /**
34
+ * Pick the `thinking` shape for a model id.
35
+ *
36
+ * Parsed from the id rather than read from the catalog on purpose: the catalog is optional
37
+ * (an engine can run with none), `buildRequest` has no access to it, and its per-model
38
+ * `reasoning` block does not currently distinguish the two shapes anyway.
39
+ *
40
+ * An unrecognised id gets `adaptive`, because `budget_tokens` is the shape being retired —
41
+ * an id we do not recognise is far likelier to be newer than us than older.
42
+ */
43
+ export declare function anthropicThinkingShape(model: string): 'adaptive' | 'budgeted';
14
44
  /** True when this Anthropic model still accepts `top_k` (see ANTHROPIC_TOP_K_MODELS). */
15
45
  export declare function anthropicAcceptsTopK(model: string): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@combycode/llm-sdk",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Unified, pluggable AI SDK for accessing the LLMs of every major provider (Anthropic, OpenAI, Google, xAI, OpenRouter) through one API. Cross-environment: Node, Bun, and the browser.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",