@combycode/llm-sdk 2.2.2 → 3.0.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/CHANGELOG.md +555 -0
- package/MIGRATION.md +130 -0
- package/dist/agent/loop-internals.d.ts +4 -0
- package/dist/agent/loop.d.ts +35 -0
- package/dist/bus/hook-bus.d.ts +13 -3
- package/dist/bus/hook-map.d.ts +24 -0
- package/dist/{llm/providers → catalog}/builtin-tools.d.ts +1 -1
- package/dist/{plugins/model-catalog → catalog}/catalog.d.ts +19 -0
- package/dist/helpers/client-pool.d.ts +1 -1
- package/dist/helpers/client-resolver.d.ts +22 -3
- package/dist/helpers/count-tokens.d.ts +1 -1
- package/dist/helpers/engine.d.ts +39 -6
- package/dist/helpers/mcp.d.ts +6 -1
- package/dist/helpers/models.d.ts +1 -1
- package/dist/helpers/one-shot.d.ts +2 -2
- package/dist/helpers/select-model.d.ts +1 -1
- package/dist/index.browser.js +17611 -4247
- package/dist/index.d.ts +5 -5
- package/dist/index.js +17611 -4247
- package/dist/llm/client-config.d.ts +9 -1
- package/dist/llm/client-internal.d.ts +11 -0
- package/dist/llm/client.d.ts +6 -0
- package/dist/llm/providers/_shared/sse.d.ts +19 -0
- package/dist/llm/providers/anthropic/batch.d.ts +13 -2
- package/dist/llm/providers/anthropic/constants.d.ts +8 -43
- package/dist/llm/providers/anthropic/files.d.ts +13 -2
- package/dist/llm/providers/anthropic/messages.d.ts +18 -1
- package/dist/llm/providers/google/batch.d.ts +14 -1
- package/dist/llm/providers/google/constants.d.ts +6 -15
- package/dist/llm/providers/google/embeddings.d.ts +8 -1
- package/dist/llm/providers/google/files.d.ts +34 -1
- package/dist/llm/providers/google/generate.d.ts +15 -1
- package/dist/llm/providers/google/interactions.d.ts +5 -1
- package/dist/llm/providers/google/media.d.ts +45 -4
- package/dist/llm/providers/google/realtime.d.ts +15 -2
- package/dist/llm/providers/openai/batch.d.ts +19 -2
- package/dist/llm/providers/openai/completions.d.ts +13 -1
- package/dist/llm/providers/openai/embeddings.d.ts +13 -1
- package/dist/llm/providers/openai/files.d.ts +13 -2
- package/dist/llm/providers/openai/media.d.ts +31 -2
- package/dist/llm/providers/openai/moderations.d.ts +11 -1
- package/dist/llm/providers/openai/realtime.d.ts +18 -2
- package/dist/llm/providers/openai/responses.d.ts +10 -2
- package/dist/llm/providers/openai/transcription.d.ts +7 -1
- package/dist/llm/providers/openrouter/completions.d.ts +5 -3
- package/dist/llm/providers/openrouter/embeddings.d.ts +3 -0
- package/dist/llm/providers/openrouter/media.d.ts +14 -4
- package/dist/llm/providers/openrouter/responses.d.ts +5 -3
- package/dist/llm/providers/xai/batch.d.ts +13 -2
- package/dist/llm/providers/xai/completions.d.ts +5 -3
- package/dist/llm/providers/xai/files.d.ts +13 -2
- package/dist/llm/providers/xai/media.d.ts +28 -10
- package/dist/llm/providers/xai/responses.d.ts +5 -3
- package/dist/llm/response-shape.d.ts +96 -0
- package/dist/llm/server-state.d.ts +1 -1
- package/dist/llm/types/options.d.ts +2 -2
- package/dist/llm/types/provider.d.ts +6 -0
- package/dist/llm/types/request.d.ts +49 -1
- package/dist/llm/wire-multipart.d.ts +26 -0
- package/dist/llm/wire-transforms.d.ts +28 -0
- package/dist/plugins/context-measurer/counter/count-api.d.ts +40 -6
- package/dist/plugins/context-measurer/counter/heuristic.d.ts +1 -1
- package/dist/plugins/context-measurer/counter/hybrid.d.ts +34 -1
- package/dist/plugins/context-measurer/counter/tiktoken.d.ts +7 -0
- package/dist/plugins/context-measurer/measurer.d.ts +1 -1
- package/dist/plugins/cost-collector/collector.d.ts +1 -1
- package/dist/plugins/cost-collector/cost-collector-internal.d.ts +1 -1
- package/dist/plugins/cost-collector/cost-collector-types.d.ts +1 -1
- package/dist/plugins/files/registry.d.ts +1 -1
- package/dist/plugins/files/strategy.d.ts +1 -1
- package/dist/plugins/internal-tools/registry.d.ts +1 -1
- package/dist/plugins/internal-tools/runner/types.d.ts +1 -1
- package/dist/plugins/mcp/sampling.d.ts +23 -1
- package/dist/plugins/mcp/transport-http.d.ts +13 -7
- package/dist/plugins/mcp/wire-rules.d.ts +21 -0
- package/dist/plugins/media/output.d.ts +1 -1
- package/dist/plugins/retrieval/document-file.d.ts +15 -0
- package/dist/plugins/retrieval/hosted-google.d.ts +8 -3
- package/dist/plugins/retrieval/hosted-openai.d.ts +9 -1
- package/dist/plugins/retrieval/hosted-xai.d.ts +8 -2
- package/dist/plugins/telemetry/telemetry.d.ts +2 -133
- package/dist/plugins/telemetry/types.d.ts +139 -0
- package/dist/util/hash.d.ts +8 -0
- package/dist/{plugins/media → util}/source-image.d.ts +1 -1
- package/dist/wire/chat-specs.d.ts +32 -0
- package/dist/wire/inherit.d.ts +43 -0
- package/dist/wire/interpreter.d.ts +279 -0
- package/dist/wire/mcp-specs.d.ts +13 -0
- package/dist/wire/media-specs.d.ts +20 -0
- package/dist/wire/pins.d.ts +34 -0
- package/dist/wire/registry.d.ts +20 -0
- package/dist/wire/retrieval-specs.d.ts +14 -0
- package/dist/wire/service-specs.d.ts +18 -0
- package/dist/wire/utility-specs.d.ts +10 -0
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,561 @@ 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
|
+
## [3.0.0] — 2026-08-24
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Exact token counting for xAI**, via `/v1/tokenize-text`. Their own SDK reaches the tokenizer over
|
|
12
|
+
gRPC (`xai_api.Tokenize/TokenizeText`), which made it look like exact counts on xAI would cost a
|
|
13
|
+
protobuf dependency and an optional peer. Asking the REST host instead: it answers 200 with the
|
|
14
|
+
same token list, so this is one more spec-built request and the library stays zero-dependency.
|
|
15
|
+
|
|
16
|
+
All seven xAI text models now declare `count_api` — measured per model rather than generalised
|
|
17
|
+
from one success: the five image and video models do not answer the endpoint and stay on the
|
|
18
|
+
heuristic. On a short Cyrillic line the estimate and the tokenizer differ by 30% on emoji and 23%
|
|
19
|
+
on code.
|
|
20
|
+
|
|
21
|
+
One distinction the guide now spells out: Anthropic and Google count the message array a
|
|
22
|
+
completion would send, so their answer is what the completion is billed for, while xAI tokenizes
|
|
23
|
+
a STRING — exact for that text, excluding the chat framing around it.
|
|
24
|
+
|
|
25
|
+
- **`createEngine({ checkResponseShapes: true })` — warn when a provider's response stops looking
|
|
26
|
+
like the one we learned to read.** A bad request returns 400 and you know at once; a bad response
|
|
27
|
+
returns 200, the parse succeeds, and the field we read is simply gone — for `usage.output_tokens`
|
|
28
|
+
that is cost reporting silently going to zero.
|
|
29
|
+
|
|
30
|
+
Four findings, on the warning bus as `response_shape_*`: a field never seen, a field that was
|
|
31
|
+
present in every recording and is now absent (what a rename looks like from outside), a
|
|
32
|
+
discriminator carrying a value nothing branches on (a new content-block type is dropped in
|
|
33
|
+
silence), and a streaming event type the parser does not handle. Off by default, never changes
|
|
34
|
+
what is parsed, and each distinct finding is reported **once per client** — a warning that repeats
|
|
35
|
+
every request is one people switch off.
|
|
36
|
+
|
|
37
|
+
The shapes are DERIVED from the recorded response corpus by `bun run derive:shapes`, never
|
|
38
|
+
hand-written, and a test re-checks every recorded body against them so the description cannot
|
|
39
|
+
drift from the recordings. Stream shapes are keyed per SSE event type: pooling them was the first
|
|
40
|
+
attempt and it cost the missing-field check entirely, since `message_start` and
|
|
41
|
+
`content_block_delta` share almost no fields.
|
|
42
|
+
|
|
43
|
+
The value of deriving rather than writing showed up immediately — a hand-written "normal
|
|
44
|
+
Anthropic response" in the first draft of the test was missing eight fields Anthropic sends on
|
|
45
|
+
every call (`stop_details`, `usage.service_tier`, `usage.cache_read_input_tokens` among them).
|
|
46
|
+
The check was right and the hand-written body was wrong.
|
|
47
|
+
|
|
48
|
+
- **A recorded corpus of what providers send BACK** (`tests/fixtures/response-golden.json`, 42 cells).
|
|
49
|
+
All seven existing corpora describe REQUESTS; the parse side was exercised only against literals
|
|
50
|
+
written by hand in the test files, which tests what the author believed a provider returns. This
|
|
51
|
+
records the real thing — the pre-parse body, or the ordered SSE events — for seven adapters across
|
|
52
|
+
six shapes (text, tool call, parallel tool calls, structured output, streaming text, streaming
|
|
53
|
+
tool call), and replays them through the same `parseResponse` / `createStreamParser` with no
|
|
54
|
+
network.
|
|
55
|
+
|
|
56
|
+
`raw` is the provider's truth and moves only when `bun run record:responses --refresh` is run;
|
|
57
|
+
`parsed` is our behaviour and is recomputed on every test run, so a parser change surfaces as a
|
|
58
|
+
failure instead of as a quiet difference in what consumers receive. Two invariants are checked
|
|
59
|
+
across all providers at once: every non-streaming response yields usage and a finish reason, and
|
|
60
|
+
every stream ends in a terminal event.
|
|
61
|
+
|
|
62
|
+
The corpus was proven to fail on a renamed provider field, on a parser that drops the finish
|
|
63
|
+
reason, and on a cell quietly disappearing. It found both fixes below in its first run.
|
|
64
|
+
|
|
65
|
+
- **The SDK's event stream is one discriminated union** (`HookEvent`). `HookMap` types a
|
|
66
|
+
subscription — `on('onCompletion', h)` has always known its own context — but the STREAM was
|
|
67
|
+
`(name, ctx: unknown)`, which pushed the type back onto the subscriber. `HookEvent` gives it one
|
|
68
|
+
variant per hook, derived from `HookMap` so the 51 cannot drift from the 51:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
hooks.onAny((e) => {
|
|
72
|
+
if (e.type === 'onCompletion') e.ctx.response?.usage; // narrowed, no cast
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This is the shape the Python and Rust ports share: a tagged union and an `enum` over the same
|
|
77
|
+
catalog. A stream typed `ctx: unknown` has no equivalent in either — the consumer's only move is
|
|
78
|
+
to cast, which is exactly what the SDK's own telemetry adapter did.
|
|
79
|
+
|
|
80
|
+
- **A model ships as callable only if it has been called.** The catalog-loader's probe was
|
|
81
|
+
advisory; 366 of 440 names had never been verified. Now `active: true` requires either a real
|
|
82
|
+
request that answered (`verifiedBy: 'probe'`) or the provider's own model list naming it
|
|
83
|
+
(`verifiedBy: 'listed'`, the only evidence available for an image or TTS model). Everything else
|
|
84
|
+
ships `active: false` — present, priced, but not offered to `selectModel`.
|
|
85
|
+
|
|
86
|
+
A name is called ONCE: later runs check the listing instead, which is free. A verified name that
|
|
87
|
+
disappears from the list is marked deprecated rather than re-probed. Verification went from
|
|
88
|
+
74/440 to 395/470.
|
|
89
|
+
|
|
90
|
+
- **`scripts/pin-catalog.ts` and `freeze-wire-golden.ts --add-new`** — a catalog import brings
|
|
91
|
+
models with no wire-spec pin and no entry in the frozen request corpus, and the library asserts
|
|
92
|
+
both. Deriving the pin and appending only the unseen models keeps those invariants true without
|
|
93
|
+
re-freezing a baseline that exists to be stable.
|
|
94
|
+
|
|
95
|
+
- **The last four hand-built surfaces are spec-driven**: exact token counting, live model listing,
|
|
96
|
+
file-content retrieval and the provenance check. With those, **every request the library sends
|
|
97
|
+
comes from a spec** — 145 of them. Nothing in `src/` assembles a URL, a header set or a body by
|
|
98
|
+
hand any more.
|
|
99
|
+
|
|
100
|
+
- **MCP is spec-driven too, transport and OAuth.** The Streamable-HTTP transport's five requests
|
|
101
|
+
(call, notification, long-lived subscription, event stream, session delete) and the OAuth flow's
|
|
102
|
+
five (two discovery probes, dynamic client registration, code exchange, refresh) now come from 13
|
|
103
|
+
specs — as does the authorization URL the user's browser opens.
|
|
104
|
+
|
|
105
|
+
This is the point where the spec format stops being about LLM providers. A JSON-RPC envelope,
|
|
106
|
+
era-dependent routing headers, a form-urlencoded token grant and an SSE stream are all described
|
|
107
|
+
with the constructs a chat request already used.
|
|
108
|
+
|
|
109
|
+
Verified against two frozen corpora (23 transport artifacts, 11 OAuth), both shown to fail on
|
|
110
|
+
deliberate corruption, plus live runs of the MCP protocol example and the five-provider MCP tool
|
|
111
|
+
scenario. The OAuth half has no live coverage anywhere — that needs a real authorization server
|
|
112
|
+
and a browser — so its frozen bytes are the only oracle it has, which the wire README now says
|
|
113
|
+
out loud.
|
|
114
|
+
|
|
115
|
+
- **Three more spec constructs**, each added because a real request needed it: `bodyKind: 'form'`
|
|
116
|
+
(the spec carries the FIELDS, the runtime encodes them — the same split multipart already used),
|
|
117
|
+
a header entry with `spread` (merge an evaluated object of headers, so a caller's header map and
|
|
118
|
+
a resolved bearer keep their precedence), and `queryEncoding: 'form'` (a space as `+` rather than
|
|
119
|
+
`%20`, which is what RFC 6749 prescribes for an authorization request and what the library
|
|
120
|
+
already sent).
|
|
121
|
+
|
|
122
|
+
- `NormalizedRequest.wireSpec` — the catalog's pin, resolved by `LLMClient` and read by
|
|
123
|
+
the adapter. Absent for an uncatalogued model or an engine with no catalog, in which
|
|
124
|
+
case the adapter derives the spec the way it always derived the shape.
|
|
125
|
+
|
|
126
|
+
- **`envelope.query` in the wire spec** — query parameters as data, each able to drop out on its
|
|
127
|
+
own. Splicing them into a `$join` URL only works while every parameter is present, and it left
|
|
128
|
+
the encoding to each caller: the hand-written backends disagreed about `encodeURIComponent`, so a
|
|
129
|
+
page token containing `+` paged from the wrong place and the API answered 200.
|
|
130
|
+
|
|
131
|
+
- **`$each` in array templates** — the array analogue of `$spread`, for a `$map` that has to sit
|
|
132
|
+
beside literal entries. Google's `tools` array is exactly that shape.
|
|
133
|
+
|
|
134
|
+
- **`scripts/gen-wire-registry.ts` (`bun run gen:registry`)** — regenerates the spec index from the
|
|
135
|
+
files on disk, with `--check` for CI. Specs have landed without their registry entry three times
|
|
136
|
+
now, and it fails silently: the spec becomes invisible to `WIRE_SPECS`, so the chain tests skip it
|
|
137
|
+
and the coverage audit reports it as neither referenced nor executed.
|
|
138
|
+
|
|
139
|
+
- **A live hosted-retrieval example** (`31-hosted-retrieval`), run on OpenAI and Google by the
|
|
140
|
+
quality gate. Hosted retrieval had unit tests and documentation but had never once been executed
|
|
141
|
+
against a provider, which is how both defects below survived.
|
|
142
|
+
|
|
143
|
+
### Changed
|
|
144
|
+
|
|
145
|
+
- **`HookBus.onAny` receives one event object instead of `(name, ctx)`** (BREAKING). Handlers take
|
|
146
|
+
`(event: HookEvent)`; `event.type` narrows `event.ctx`.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
hooks.onAny((name, ctx) => { ... }); // before
|
|
150
|
+
hooks.onAny((event) => { ... }); // after
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The SDK's telemetry adapter was the only consumer, and it shows why the old shape was worth
|
|
154
|
+
breaking: it opened with `const c = ctx as Record<string, unknown>` and then cast per field —
|
|
155
|
+
`(c.response as { usage?: … })?.usage`, `c.latencyMs as number`, `(c.error as Error)?.message`.
|
|
156
|
+
Rename a context field and every one of those keeps compiling and quietly reads `undefined`,
|
|
157
|
+
which for the token and cost fields is a metric that silently goes to zero. All 26 casts in that
|
|
158
|
+
method are gone (0 left); the switch narrows instead, and the compiler now checks each field against the
|
|
159
|
+
context it actually belongs to.
|
|
160
|
+
|
|
161
|
+
Nothing is allocated when no catch-all is subscribed, so the per-chunk hot path is unchanged.
|
|
162
|
+
- **The catalog is current again, and the chain that maintains it works end to end.** The
|
|
163
|
+
catalog-loader's export pointed at the catalogs' old home, which was BOTH its write target and
|
|
164
|
+
its merge base — so the "never drop a shipped model, never blank a price" guarantee silently
|
|
165
|
+
guaranteed nothing, and a run would have dropped 10 models and all 284 `wireSpec` pins. Repointed,
|
|
166
|
+
and it now refuses to run at all if the merge base is missing.
|
|
167
|
+
|
|
168
|
+
With it fixed, a full run brought the catalog up to date: +99 models discovered, 57 prices
|
|
169
|
+
changed (all four provider pricing pages had moved), 42 tokenizer strategies delivered, 61 models
|
|
170
|
+
marked `active: false`, and 48 given a deprecation date the sources announced. The `wire` field
|
|
171
|
+
removed in 3.0.0 was still riding along on 26 entries and is now dropped.
|
|
172
|
+
|
|
173
|
+
- **The catalog is loaded by default** (BREAKING, behaviour). `createEngine()` and `LLMClient` used
|
|
174
|
+
to start with an EMPTY catalog unless the caller passed `catalog: 'defaults'`. Three things fell
|
|
175
|
+
back silently as a result: the wire spec was derived from the model id instead of read from its
|
|
176
|
+
pin, every price was unknown, and every token count was the 4-chars-per-token estimate. Each is
|
|
177
|
+
the right answer for a model this build has never heard of, which is why nothing looked wrong.
|
|
178
|
+
|
|
179
|
+
This is the prerequisite the 3.0.0 design named (report 037, R1): the adapters can only be driven
|
|
180
|
+
by per-model data if that data is actually there. The bundled catalogs are statically imported
|
|
181
|
+
either way, so leaving them unloaded never saved a byte — it cost about a millisecond of indexing
|
|
182
|
+
per engine and bought silence.
|
|
183
|
+
|
|
184
|
+
Opting out is now the explicit act: `catalog: false` or `catalog: 'empty'`.
|
|
185
|
+
|
|
186
|
+
- **`AnthropicCountApi` and `GoogleCountApi` take an `EngineFetch`, and it is required** (BREAKING).
|
|
187
|
+
They defaulted to `globalThis.fetch`, so every exact token count for Anthropic and Google went out
|
|
188
|
+
AROUND the NetworkEngine: no queue, no rate limiting, no retry, no telemetry span — while every
|
|
189
|
+
other file in the library states that all HTTP goes through the injected fetch. `countTokens()`
|
|
190
|
+
passes `engine.fetch` for you, so the documented path needs no change; a `HybridTokenCounter`
|
|
191
|
+
built with `countApiKeys` but no `fetch` now says so and falls back to the heuristic instead of
|
|
192
|
+
silently leaving the engine.
|
|
193
|
+
|
|
194
|
+
The default is gone rather than replaced, because a default that silently bypasses the engine is
|
|
195
|
+
what produced this.
|
|
196
|
+
|
|
197
|
+
- **MCP header assembly is one ordered list instead of three helpers.** Which headers a call
|
|
198
|
+
carries — session, protocol version, the modern `Mcp-Method` / `Mcp-Name` routing pair — was
|
|
199
|
+
decided by three private methods and by the order their results were spread into an object
|
|
200
|
+
literal. It is now a declared sequence, and the two rules that genuinely are not data (era
|
|
201
|
+
detection, and reading a subject from a different param per method) are named registry entries
|
|
202
|
+
the coverage audit executes.
|
|
203
|
+
|
|
204
|
+
One asymmetry was preserved rather than tidied: a NOTIFICATION carries no routing headers, which
|
|
205
|
+
is what the transport has always sent. The freeze caught the attempt to "fix" it, and there is no
|
|
206
|
+
modern server here to test the change against.
|
|
207
|
+
|
|
208
|
+
- **The chat adapters build their requests from the wire specs.** All five —
|
|
209
|
+
`anthropic/messages`, `google/generateContent`, `google/interactions`,
|
|
210
|
+
`openai/responses` and `openai/chat-completions`, the last two also covering the xAI
|
|
211
|
+
and OpenRouter flavors — now interpret the spec the catalog pins the model to, instead
|
|
212
|
+
of assembling the body by hand. 806 lines of request-building code became 147.
|
|
213
|
+
|
|
214
|
+
The three OpenAI-compatible subclasses are the clearest case: xAI and OpenRouter each
|
|
215
|
+
overrode `buildRequest` to call `super`, then rename `max_tokens`, strip `reasoning`,
|
|
216
|
+
remap the service tier and merge routing options. Every one of those edits is already
|
|
217
|
+
the flavor overlay in the shared spec, so naming the flavor is now the entire override.
|
|
218
|
+
|
|
219
|
+
**The wire did not move.** A corpus frozen from 2.3.0 — 290 subjects x 22 request
|
|
220
|
+
shapes, both the pinned and the id-derived route — is compared on every CI run, and all
|
|
221
|
+
12,760 comparisons are byte-identical to what 2.3.0 sent. Verified live against all five
|
|
222
|
+
providers as well; the specs are proven, not assumed.
|
|
223
|
+
|
|
224
|
+
Two supporting moves: `wire-transforms` left `src/wire/` (it imports from `src/llm`, and
|
|
225
|
+
the new edge would otherwise have made a cycle), and the runtime loads a chat-only spec
|
|
226
|
+
set rather than the full 71-spec index, so nothing is bundled that nothing executes.
|
|
227
|
+
Cost: +15 KB packed, and ~5 microseconds per request against a network call.
|
|
228
|
+
|
|
229
|
+
- **Model-band selection is data, not code.** Which chain node an UNPINNED model uses now comes
|
|
230
|
+
from `src/wire/pins/*.json` — ordered regex rules plus a default — instead of version arithmetic
|
|
231
|
+
written in TypeScript. The Python and Rust ports read the same file rather than each
|
|
232
|
+
re-implementing the rule and drifting from it, which is how 2.2.1 happened.
|
|
233
|
+
|
|
234
|
+
The fallback itself is unchanged and still matters: it is how the SDK behaves for a model
|
|
235
|
+
released after this build, and for any engine run without a catalog. Verified against every id
|
|
236
|
+
the previous code handled, plus dated snapshots, legacy family-last ids, and plausible future
|
|
237
|
+
releases.
|
|
238
|
+
|
|
239
|
+
One deliberate fix came out of it: the old pair of helpers disagreed on case — one lower-cased
|
|
240
|
+
the model id and the other did not — so `CLAUDE-OPUS-4-6` lost a `top_k` that model accepts. The
|
|
241
|
+
pin table lower-cases consistently.
|
|
242
|
+
|
|
243
|
+
- **The hosted retrieval backends build their requests from wire specs.** OpenAI vector stores,
|
|
244
|
+
Google file search stores and xAI Grok collections — 23 hand-assembled requests across three
|
|
245
|
+
files — now come from 24 specs. This was the largest remaining block of request construction
|
|
246
|
+
written three times over, once per provider.
|
|
247
|
+
|
|
248
|
+
xAI shows why it matters: collections span two hosts with two separate credentials, and which
|
|
249
|
+
pair a call used was decided by whichever bearer helper the author typed next to the URL. It is
|
|
250
|
+
now a property of the endpoint, declared in the spec.
|
|
251
|
+
|
|
252
|
+
Verified against a corpus frozen from the pre-migration commit — 41 requests, byte-identical —
|
|
253
|
+
and by a live end-to-end run on both providers.
|
|
254
|
+
|
|
255
|
+
### Fixed
|
|
256
|
+
|
|
257
|
+
- **An explicit `provider` was ignored whenever the model id contained a slash — sending the API key
|
|
258
|
+
to the wrong company.** `resolveModel` read the model's `vendor/` prefix first and fell back to the
|
|
259
|
+
explicit argument only for a bare id. Every OpenRouter model id is `vendor/model`, so the most
|
|
260
|
+
ordinary OpenRouter call there is —
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
createLLM({ provider: 'openrouter', model: 'openai/gpt-5.4-nano', apiKey })
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
— resolved to the provider `openai` and sent the **OpenRouter key to api.openai.com**, which
|
|
267
|
+
answered `Incorrect API key provided: sk-or-v1…`. A vendor outside our five failed differently and
|
|
268
|
+
no better: the prefix was cast to a `ProviderName`, so `qwen/qwen3` produced a provider literally
|
|
269
|
+
named `qwen` and died later as "no default adapter for provider 'qwen'".
|
|
270
|
+
|
|
271
|
+
An explicit provider now wins, and a redundant leading `<provider>/` is stripped, so the catalog's
|
|
272
|
+
own `openrouter/openai/gpt-5.4-nano` slug resolves to the OpenRouter model `openai/gpt-5.4-nano`.
|
|
273
|
+
Prefix parsing without a provider is unchanged and stays permissive — `estimate()` prices models
|
|
274
|
+
catalogued under providers nobody can call. Affects all nine call sites: `createLLM`, `batch`,
|
|
275
|
+
`embed`, `moderate`, `transcribe`, `createRealtime`, `countTokens`, `estimate`, `estimator`.
|
|
276
|
+
|
|
277
|
+
- **Google's `responseId` was thrown away and replaced with a random UUID.** `parseResponse` minted
|
|
278
|
+
`crypto.randomUUID()` under a comment claiming generateContent returns no id — it returns
|
|
279
|
+
`responseId` at the top level, and every recorded response carries one. Two consequences: the parse
|
|
280
|
+
was not deterministic, so the same bytes produced a different `response.id` each time and nothing
|
|
281
|
+
keyed on it could correlate; and a cache hit, which replays the stored body, reported a different
|
|
282
|
+
id than the call that populated it. The provider's id is now used, with the generated one kept as
|
|
283
|
+
a fallback for older payloads.
|
|
284
|
+
|
|
285
|
+
- **`batch`, `embed`, `transcribe` and `moderate` sent our SLUG instead of the provider's id.**
|
|
286
|
+
Only `createLLM` translated through the catalog, so those four worked purely for models whose
|
|
287
|
+
canonical id happens to be the callable one. The moment the sample corpus moved to
|
|
288
|
+
`claude-haiku-4.5`, every batch request came back `not_found_error: model: claude-haiku-4.5` —
|
|
289
|
+
two requests, zero successes, and an exit code of 0 to go with it.
|
|
290
|
+
|
|
291
|
+
All four now send `providerModelName` while keeping the slug for pricing and catalog lookups,
|
|
292
|
+
which are keyed by it. A test asserts the distinction and was checked against the unfixed code.
|
|
293
|
+
|
|
294
|
+
- **Exact token counting works, for the first time.** `HybridTokenCounter` picks its strategy from
|
|
295
|
+
the catalog's `tokenizer.strategy`, and no shipped model declared one — so every count fell back
|
|
296
|
+
to the 4-chars-per-token estimate and neither exact path had ever run. All 455 models now carry a
|
|
297
|
+
strategy: `count_api` for Anthropic and Google chat models, `tiktoken` for OpenAI, `heuristic`
|
|
298
|
+
elsewhere. On one Cyrillic line the difference is 9 estimated versus **19 actual**.
|
|
299
|
+
|
|
300
|
+
Two defects surfaced the moment the strategy was selected:
|
|
301
|
+
|
|
302
|
+
- The count endpoint was sent our canonical SLUG (`claude-haiku-4.5`) rather than the callable id
|
|
303
|
+
(`claude-haiku-4-5-20251001`), so the first model whose ids differed answered 404. The chat path
|
|
304
|
+
translates through the catalog; this one did not.
|
|
305
|
+
- A model marked `tiktoken` threw when the optional `tiktoken` peer was not installed, turning a
|
|
306
|
+
number into an error for anyone who had not opted in — while the guide promised the opposite.
|
|
307
|
+
The counter now falls back to the heuristic and says so once. Only that specific error is
|
|
308
|
+
caught: a network failure inside the count API still surfaces, because quietly answering with
|
|
309
|
+
an estimate when an exact count was asked for is how a wrong number gets believed.
|
|
310
|
+
|
|
311
|
+
- **xAI hosted retrieval said "ready" before anything was searchable.** `indexStatus()` derived
|
|
312
|
+
readiness from the collection's `documents_count`, which reaches 1 the moment a document is
|
|
313
|
+
ATTACHED — measured at about five seconds before that document can actually be found. A caller
|
|
314
|
+
that polled exactly as the guide instructs still searched an empty index, and the model answered
|
|
315
|
+
from its own knowledge with nothing to say why.
|
|
316
|
+
|
|
317
|
+
It now reads the per-document status the API actually exposes (`DOCUMENT_STATUS_PROCESSING` ->
|
|
318
|
+
`PROCESSED`), so `ready` means searchable, and a partial failure reports `error` instead of a
|
|
319
|
+
quietly smaller corpus. xAI now passes the live hosted-retrieval scenario.
|
|
320
|
+
|
|
321
|
+
Found with it: `listCorpora()` threw a `TypeError` on every real response — the API returns
|
|
322
|
+
`{ collections: [...] }` and the code expected a bare array, as did the test fake. Both defects
|
|
323
|
+
survived because the unit tests were written against invented response shapes; they now use the
|
|
324
|
+
ones captured from the live API.
|
|
325
|
+
|
|
326
|
+
- **The Google API key no longer travels in the URL.** Twelve endpoints — files, batch, media
|
|
327
|
+
generation, Imagen, Veo and the long-running-operation polls — sent it as `?key=`, so the
|
|
328
|
+
credential was copied into every access log, proxy log and telemetry span the request passed
|
|
329
|
+
through, and could leak through a `Referer`. They now send `x-goog-api-key`, which is what the
|
|
330
|
+
chat adapter was already fixed to do. One endpoint was sending it BOTH ways.
|
|
331
|
+
|
|
332
|
+
`google/realtime` still uses `?key=` and is the one documented exemption: it is a WebSocket
|
|
333
|
+
handshake and a browser cannot set a header on one. A test enumerates the exemptions and fails if
|
|
334
|
+
one becomes stale, so the next `?key=` cannot arrive quietly.
|
|
335
|
+
|
|
336
|
+
Verified live before the fixtures were re-frozen — google files, image, tts and batch all pass
|
|
337
|
+
with header auth — and the re-freeze was diffed pairwise: 21 artifacts changed, every one only in
|
|
338
|
+
that way.
|
|
339
|
+
|
|
340
|
+
- **The cost ledger recorded a provider call that never happened.** `countTokens()` emitted its
|
|
341
|
+
zero-cost count-API entry whenever the provider was Anthropic or Google and a key was present —
|
|
342
|
+
that is INTENT. The counter picks its strategy from the catalog's `tokenizer.strategy`, and no
|
|
343
|
+
catalogued model declares one, so the heuristic answered and nothing was called. The entry is now
|
|
344
|
+
gated on the strategy that actually ran.
|
|
345
|
+
|
|
346
|
+
- **A test that was describing the bug.** The count-API cost test stubbed `globalThis.fetch` and
|
|
347
|
+
handed the engine a `null` one — it could only pass while the count APIs bypassed the engine. It
|
|
348
|
+
now intercepts the engine's fetch and asserts the endpoint that was called.
|
|
349
|
+
|
|
350
|
+
- **A Google hosted corpus was silently ignored.** `{ type: 'file_search' }` had no mapping in the
|
|
351
|
+
Gemini chain spec, so the tool was dropped from the request and the model answered from its own
|
|
352
|
+
knowledge, with no error anywhere. The live example asks a question only the uploaded document
|
|
353
|
+
can answer and got a plausible wrong number back. `fileSearch` is now mapped from the tool's
|
|
354
|
+
params, matching `Tool.fileSearch` in Google's own SDK.
|
|
355
|
+
|
|
356
|
+
The same call on OpenAI failed loudly instead (`Missing required parameter:
|
|
357
|
+
'tools[0].vector_store_ids'`): a hosted `asTool()` result has to be passed as the `params` of a
|
|
358
|
+
`file_search` builtin. The retrieval guide now shows that, having previously said "splice into
|
|
359
|
+
the provider's native call" without saying how.
|
|
360
|
+
|
|
361
|
+
- **Hosted document uploads are reproducible.** A document with no `label` was named
|
|
362
|
+
`doc-<random-uuid>.txt`, so the same upload produced a different request every time: it could not
|
|
363
|
+
be asserted in a test, frozen in a fixture, or matched against a log, and a retried upload
|
|
364
|
+
arrived under a new name. The fallback is now derived from the document's content — the same fix
|
|
365
|
+
the xAI batch name got.
|
|
366
|
+
|
|
367
|
+
### Removed
|
|
368
|
+
|
|
369
|
+
- **`ModelInfo.wire`, `NormalizedRequest.wire` and the `ModelWire` type** (BREAKING, type-level
|
|
370
|
+
only). These carried per-model wire traits; `ModelInfo.wireSpec` carries the same knowledge and
|
|
371
|
+
carries it once. Two
|
|
372
|
+
representations of one fact drift, and this library has shipped two bugs from exactly that. See
|
|
373
|
+
MIGRATION.md — behaviour is unchanged and most codebases need no edit.
|
|
374
|
+
|
|
375
|
+
Removed with them, and never reachable from the package entry point: `anthropicThinkingShape`,
|
|
376
|
+
`anthropicAcceptsTopK`, `ANTHROPIC_ADAPTIVE_THINKING_MIN`, `ANTHROPIC_THINKING_BUDGETS`,
|
|
377
|
+
`DEFAULT_ANTHROPIC_THINKING_BUDGET`, `googleUsesThinkingBudget`, `GOOGLE_THINKING_BUDGETS`,
|
|
378
|
+
`GOOGLE_THINKING_LEVELS`.
|
|
379
|
+
|
|
380
|
+
## [2.3.0] — 2026-08-23
|
|
381
|
+
|
|
382
|
+
### Added
|
|
383
|
+
|
|
384
|
+
- **Every catalogued chat model is pinned to a wire spec.** `ModelInfo.wireSpec` names the spec
|
|
385
|
+
that builds that model's requests — `anthropic/messages@4.7`, `google/generate@2.5`,
|
|
386
|
+
`openai/responses`, and so on. All 289 chat models across five providers carry one.
|
|
387
|
+
|
|
388
|
+
The pin is what lets this SDK and the Python and Rust ports agree on a model without each
|
|
389
|
+
re-deriving its wire shape from the model id — the derivation that produced the 2.2.1 and 2.2.2
|
|
390
|
+
bugs.
|
|
391
|
+
|
|
392
|
+
Carried and validated, not yet authoritative: the hand-written adapters still build requests
|
|
393
|
+
from `wire` traits. Because that is two representations of one fact, and two representations
|
|
394
|
+
drift, a test drives the PINNED SPEC and asserts the request it produces matches what the traits
|
|
395
|
+
say the model takes. When the adapters become spec-driven in 3.0.0, `wire` goes away and that
|
|
396
|
+
test is what makes the swap safe.
|
|
397
|
+
|
|
398
|
+
- **Wire specs ship in the repo** (`src/wire/`). 71 JSON specs describe how to talk to each
|
|
399
|
+
provider API — field names, enum values, defaults, versioned tool-type strings, which shape a
|
|
400
|
+
model version takes — covering every adapter the SDK has: chat, interactions, media, realtime,
|
|
401
|
+
embeddings, files and batch.
|
|
402
|
+
|
|
403
|
+
They exist so the Python and Rust ports consume one artifact instead of re-deriving the same
|
|
404
|
+
knowledge three times, and so a provider change is one reviewable diff rather than three code
|
|
405
|
+
changes. This is the knowledge that, living in regexes, produced the 2.2.1 and 2.2.2 bugs.
|
|
406
|
+
|
|
407
|
+
**Oracle, not yet authority:** the adapters remain hand-written, and a test
|
|
408
|
+
(`tests/unit/wire`) proves the specs and the adapters agree on every CI run — data that is
|
|
409
|
+
never executed rots. Making the specs authoritative is the 3.0.0 step.
|
|
410
|
+
|
|
411
|
+
They are not exported from `index.ts` and are tree-shaken out of `dist`, so they add **no bytes**
|
|
412
|
+
to the published package (verified: package size unchanged).
|
|
413
|
+
|
|
414
|
+
- **Every catalogued chat model is checked against its pinned spec on every run.** The previous
|
|
415
|
+
pin test drove Anthropic and Google models only — 26 of 289. The other 263, 224 of them
|
|
416
|
+
OpenRouter, were covered by nothing stronger than "the pin names a spec that resolves", which a
|
|
417
|
+
typo satisfies. `tests/unit/wire/every-model-reproduces-its-adapter.test.ts` now builds 17
|
|
418
|
+
request shapes for every chat model, through the spec the CATALOG pins it to, and requires the
|
|
419
|
+
payload to equal the one the real adapter produces — ~4,900 comparisons, under a second.
|
|
420
|
+
|
|
421
|
+
The adapter is chosen from `preferredApi`, never from the pin. Choosing it from the pin is
|
|
422
|
+
circular and silently so: mis-pin an OpenRouter model to `openai/responses` and the adapter
|
|
423
|
+
moves with it, both sides agree, and the sweep stays green on a broken pin. It did exactly that
|
|
424
|
+
until a deliberate corruption caught it. Mis-pinning any single model in any of the five
|
|
425
|
+
providers now fails, as does a provider losing its pins entirely.
|
|
426
|
+
|
|
427
|
+
- **A consumer example for traces** (`telemetry-traces`, in the examples corpus): subscribing with
|
|
428
|
+
`onTrace`, filtering by span type at the subscription rather than in the handler, head sampling,
|
|
429
|
+
keeping prompt content out, joining an inbound `traceparent`, naming a run with
|
|
430
|
+
`label`/`source`/`attributes`, and reading `client.routing`. The trace feed had no example at
|
|
431
|
+
all, which the quality gate's example-first check was reporting.
|
|
432
|
+
|
|
433
|
+
- **Request builders on the media and realtime adapters.** These adapters used to assemble each
|
|
434
|
+
request *inside* the method that also fetched and parsed it, so the only way to see what the SDK
|
|
435
|
+
would send was to intercept the network. Construction is now separated:
|
|
436
|
+
|
|
437
|
+
- `GoogleMediaAdapter`: `buildImageRequest`, `buildEditImageRequest`, `buildAudioRequest`,
|
|
438
|
+
`buildVideoRequest`, plus the lower-level `buildImagenRequest` / `buildGenerateContentRequest`.
|
|
439
|
+
- `OpenAIMediaAdapter`: `buildGenerateImageRequest`, `buildEditImageRequest`, `buildAudioRequest`,
|
|
440
|
+
`buildVideoRequest`.
|
|
441
|
+
- Realtime: `buildConnectRequest` on both adapters, and free functions
|
|
442
|
+
`buildOpenAISessionUpdate` / `buildOpenAITurnFrames` and `buildGoogleSetupFrame` /
|
|
443
|
+
`buildGoogleTurnFrames` for the handshake and per-turn frames.
|
|
444
|
+
|
|
445
|
+
The public methods now call these, so the two cannot drift — and a test
|
|
446
|
+
(`media-request-builders.test.ts`) asserts that what a builder returns is byte-identical to what
|
|
447
|
+
its method actually sends.
|
|
448
|
+
|
|
449
|
+
Additive: no existing signature changed.
|
|
450
|
+
|
|
451
|
+
- **The catalog now knows how to TALK to a model, not just what it can do.** `ModelInfo` gains a
|
|
452
|
+
`wire` block carrying per-model wire traits — which `thinking` shape the model accepts, whether
|
|
453
|
+
it takes `top_k` — and `LLMClient` resolves it onto every request as `NormalizedRequest.wire`.
|
|
454
|
+
Adapters read that instead of parsing the model id.
|
|
455
|
+
|
|
456
|
+
This is the gap behind two shipped bugs. The catalog already recorded that a model supported
|
|
457
|
+
reasoning; nothing recorded which of two incompatible `thinking` shapes it accepted, so adapters
|
|
458
|
+
matched on the id and got it wrong twice — in 2.2.1, and again in the 4.0 date-suffix defect
|
|
459
|
+
fixed this release. Wire knowledge is now reviewable data that can be diffed and generated,
|
|
460
|
+
rather than a regex nobody re-reads.
|
|
461
|
+
|
|
462
|
+
The bundled catalog carries `wire` for all 14 Anthropic and 12 Google chat models, generated
|
|
463
|
+
from the existing rules so behaviour is unchanged on day one. When the catalog is silent —
|
|
464
|
+
an engine running without one, or an uncatalogued model — adapters fall back to parsing the id
|
|
465
|
+
exactly as before, so nothing breaks. Removing that fallback is a later step, once every model
|
|
466
|
+
is pinned.
|
|
467
|
+
|
|
468
|
+
- `EngineHandle.createClient(options)` — build an `LLMClient` bound to that engine. Added so
|
|
469
|
+
`plugins/internal-tools` can obtain a client without importing `createLLM` from the helpers
|
|
470
|
+
layer. Additive on a handle callers receive rather than implement.
|
|
471
|
+
|
|
472
|
+
- `util/hash` — FNV-1a 32-bit, deterministic and dependency-free, for deriving stable short ids
|
|
473
|
+
from content instead of from a clock.
|
|
474
|
+
|
|
475
|
+
### Changed
|
|
476
|
+
|
|
477
|
+
- **The module graph is now a DAG.** Two dependency cycles between top-level layers were closed:
|
|
478
|
+
`llm <-> plugins` and `helpers <-> plugins`. They were harmless in TypeScript and are not
|
|
479
|
+
harmless in Rust, where crates cannot express a cycle, so they blocked the port.
|
|
480
|
+
|
|
481
|
+
Shared code moved DOWN rather than sideways: the model catalog and its bundled data now live in
|
|
482
|
+
`src/catalog/` instead of `src/plugins/model-catalog/` + `src/llm/providers/*/catalog.json`, and
|
|
483
|
+
image-source normalisation moved from `src/plugins/media/source-image` to `src/util/source-image`.
|
|
484
|
+
Where a lower layer genuinely needs a capability from a higher one it is now passed down instead
|
|
485
|
+
of imported up.
|
|
486
|
+
|
|
487
|
+
**No public API changed** — the package has a single root export and every moved symbol is
|
|
488
|
+
re-exported from the same place as before. A new test (`tests/unit/architecture/layers.test.ts`)
|
|
489
|
+
fails if any cycle returns.
|
|
490
|
+
|
|
491
|
+
- **Internal cleanups carried over from the 1.0 backlog.**
|
|
492
|
+
- `buildContext` read `LLMClient`'s private `queueName` / `configName` / `cacheName` through
|
|
493
|
+
`as unknown as` casts, which compile happily and would silently yield `undefined` the day a
|
|
494
|
+
field is renamed. The client now exposes them deliberately as `client.routing`, so a rename is
|
|
495
|
+
a type error.
|
|
496
|
+
- Telemetry's ten exported types moved from the 1,225-line `telemetry.ts` to
|
|
497
|
+
`plugins/telemetry/types.ts`, matching the rest of the codebase. Re-exported from the old path,
|
|
498
|
+
so no import — public or internal — changed.
|
|
499
|
+
- `sseJson()` in `providers/_shared` replaces the one line every provider's stream parser
|
|
500
|
+
repeated verbatim.
|
|
501
|
+
|
|
502
|
+
- **`providerOptions` is typed.** It was `Record<string, unknown>` — the one untyped hole in the
|
|
503
|
+
request, and so the one place a typo produced silence rather than an error:
|
|
504
|
+
`promtCacheOptions` type-checked and was simply never sent.
|
|
505
|
+
|
|
506
|
+
The new `ProviderOptions` interface documents every key an adapter actually reads
|
|
507
|
+
(`userProfileId`; `moderationPolicy`, `promptCacheOptions`, `reasoningMode`;
|
|
508
|
+
`responseModalities`, `speechConfig`, `imageConfig`, `translationConfig`, `cachedContent`;
|
|
509
|
+
`openrouter`), derived from the read sites rather than invented. Two `as` casts at those sites
|
|
510
|
+
became unnecessary and were removed.
|
|
511
|
+
|
|
512
|
+
**Not breaking:** the index signature stays, so an unmodelled key is still accepted — providers
|
|
513
|
+
ship parameters before the SDK models them, and refusing those would make the escape hatch
|
|
514
|
+
useless. What changed is that the keys we do know are checked and discoverable.
|
|
515
|
+
|
|
516
|
+
- **`AgentLoop.complete()` and `stream()` no longer duplicate their scaffolding.** The two are the
|
|
517
|
+
same loop with different plumbing, and they had drifted into near-duplicates — 167 identical
|
|
518
|
+
lines across ~560. Five shared pieces are now extracted: `recordRunError`, `resolveFinalText`,
|
|
519
|
+
`buildFinalResponse`, `settleRun`, and `buildStepOptions`.
|
|
520
|
+
|
|
521
|
+
This is not tidying. The `ctx` block in `buildStepOptions` is what stops one conversation
|
|
522
|
+
arriving at a collector as several unrelated traces; duplicated, a fix to one path would have
|
|
523
|
+
left the other silently splitting. The same applies to the run's final-text rules and its
|
|
524
|
+
error reporting.
|
|
525
|
+
|
|
526
|
+
`complete()` 303 -> 234 lines, `stream()` 261 -> 209, identical shared lines 167 -> ~118.
|
|
527
|
+
Behaviour unchanged: all 1,953 tests pass untouched.
|
|
528
|
+
|
|
529
|
+
### Fixed
|
|
530
|
+
|
|
531
|
+
- **Anthropic 4.0 models were handed the 4.6+ `thinking` shape.** `anthropicThinkingShape()`
|
|
532
|
+
parsed a model id with `/^claude-[a-z]+-(\d+)(?:[-.](\d+))?/`, so an id carrying a release date
|
|
533
|
+
but no minor version read the date AS the minor: `claude-opus-4-20250514` became major 4 /
|
|
534
|
+
minor 20250514, cleared the `>= 4.6` test, and was sent `thinking: {type:'adaptive'}` — the
|
|
535
|
+
opposite of what the file's own `ANTHROPIC_ADAPTIVE_THINKING_MIN = {major:4, minor:6}` says.
|
|
536
|
+
|
|
537
|
+
This never shipped a failure: `claude-opus-4-20250514` and `claude-sonnet-4-20250514` are the
|
|
538
|
+
only affected ids and both are `active: false` in the catalog (deprecated 2026-05-14). It is
|
|
539
|
+
nonetheless the 2.2.1 regression in mirror image, so it is fixed rather than left latent. The
|
|
540
|
+
minor is now bounded to one or two digits and must not be followed by another digit. Checked
|
|
541
|
+
against every id in the catalog plus aliases and future-shaped ids (`claude-opus-4-10`,
|
|
542
|
+
`claude-sonnet-6-1-20270101`): exactly those two change classification, the other 17 are
|
|
543
|
+
untouched.
|
|
544
|
+
|
|
545
|
+
- **xAI batch creation was not reproducible.** The create call named the batch
|
|
546
|
+
`` `batch_${Date.now()}` ``, which made it the only request in the provider surface that was not
|
|
547
|
+
a pure function of its input: it could not be asserted in a test or reproduced from a log, and a
|
|
548
|
+
retried create produced a second batch under a different name that nothing could deduplicate.
|
|
549
|
+
The name is now derived from the batch contents, so identical submissions produce identical
|
|
550
|
+
requests and differing ones still differ.
|
|
551
|
+
|
|
552
|
+
- **`google/files` rejected Google's own resource-name format.** `delete()` and `getInfo()`
|
|
553
|
+
normalised the file id only when it contained `/files/` *with* a leading slash — true of the
|
|
554
|
+
full `uri` this adapter returns from `upload()` and `list()`, and false of `files/abc`, the
|
|
555
|
+
canonical `name` the Google API itself returns. Passing that back produced
|
|
556
|
+
`/v1beta/files/files/abc` and a 404.
|
|
557
|
+
|
|
558
|
+
It never broke the library's own round-trip, which is why it survived: the only way to reach it
|
|
559
|
+
was to use the provider's own id format. All three forms — full uri, `files/abc`, and a bare
|
|
560
|
+
name — now normalise to the same request.
|
|
561
|
+
|
|
7
562
|
## [2.2.2] — 2026-08-17
|
|
8
563
|
|
|
9
564
|
### Fixed
|