@combycode/llm-sdk 1.6.1 → 2.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +518 -1
  2. package/MIGRATION.md +93 -0
  3. package/README.md +17 -2
  4. package/dist/agent/loop-config.d.ts +22 -0
  5. package/dist/agent/loop-step-state.d.ts +2 -0
  6. package/dist/agent/loop.d.ts +7 -0
  7. package/dist/agent/reflect-retry.d.ts +56 -0
  8. package/dist/agent/tool-key.d.ts +3 -0
  9. package/dist/bus/hook-map.d.ts +11 -0
  10. package/dist/helpers/mcp.d.ts +24 -2
  11. package/dist/helpers/provenance-types.d.ts +63 -0
  12. package/dist/helpers/provenance.d.ts +12 -0
  13. package/dist/helpers/transcribe.d.ts +35 -6
  14. package/dist/index.browser.js +3134 -731
  15. package/dist/index.d.ts +19 -7
  16. package/dist/index.js +3134 -731
  17. package/dist/llm/moderation/native.d.ts +5 -4
  18. package/dist/llm/providers/anthropic/constants.d.ts +2 -0
  19. package/dist/llm/providers/google/constants.d.ts +17 -2
  20. package/dist/llm/providers/openai/completions.d.ts +18 -2
  21. package/dist/llm/providers/openai/provenance.d.ts +26 -0
  22. package/dist/llm/providers/openai/responses.d.ts +4 -2
  23. package/dist/llm/providers/openai/transcription.d.ts +39 -2
  24. package/dist/llm/providers/xai/completions.d.ts +2 -2
  25. package/dist/llm/providers/xai/media.d.ts +8 -0
  26. package/dist/llm/types/audio.d.ts +31 -0
  27. package/dist/llm/types/messages.d.ts +89 -1
  28. package/dist/llm/types/options.d.ts +15 -0
  29. package/dist/llm/types/request.d.ts +21 -1
  30. package/dist/llm/types/response.d.ts +31 -1
  31. package/dist/llm/types/stream.d.ts +14 -1
  32. package/dist/llm/types/tiers.d.ts +6 -6
  33. package/dist/llm/types/tools.d.ts +10 -1
  34. package/dist/network/queue-state-config.d.ts +5 -0
  35. package/dist/network/queue-state.d.ts +7 -0
  36. package/dist/network/types.d.ts +23 -0
  37. package/dist/plugins/context-guard/strategies/anchored.d.ts +47 -0
  38. package/dist/plugins/context-measurer/counter/hybrid.d.ts +4 -1
  39. package/dist/plugins/context-measurer/counter/tiktoken.d.ts +8 -1
  40. package/dist/plugins/mcp/base-transport.d.ts +16 -0
  41. package/dist/plugins/mcp/client.d.ts +144 -7
  42. package/dist/plugins/mcp/input-required.d.ts +35 -0
  43. package/dist/plugins/mcp/jsonrpc.d.ts +7 -0
  44. package/dist/plugins/mcp/oauth.d.ts +21 -1
  45. package/dist/plugins/mcp/protocol-version.d.ts +61 -0
  46. package/dist/plugins/mcp/result-cache.d.ts +31 -0
  47. package/dist/plugins/mcp/subscriptions.d.ts +69 -0
  48. package/dist/plugins/mcp/transport-http.d.ts +31 -0
  49. package/dist/plugins/mcp/transport-stdio.d.ts +2 -0
  50. package/dist/plugins/mcp/transport-ws.d.ts +11 -1
  51. package/dist/plugins/mcp/transport.d.ts +11 -0
  52. package/dist/plugins/mcp/types.d.ts +54 -2
  53. package/dist/plugins/media/source-image.d.ts +9 -0
  54. package/dist/plugins/media/types.d.ts +21 -0
  55. package/dist/plugins/model-catalog/catalog.d.ts +3 -0
  56. package/dist/plugins/telemetry/telemetry.d.ts +12 -0
  57. package/dist/util/http.d.ts +8 -0
  58. package/package.json +9 -6
package/CHANGELOG.md CHANGED
@@ -4,7 +4,524 @@ 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
- ## [Unreleased]
7
+ ## [2.0.0] - 2026-08-09
8
+
9
+ **Upgrading:** three things can require action, and none of them is a provider change — that is the
10
+ point of the facade. (1) Node **22+** is now required. (2) `tiktoken` is an optional **peer**: run
11
+ `npm i tiktoken` only if you use exact local OpenAI token counting. (3) If you switch exhaustively
12
+ over `FinishReason` or `ContentPart` without a `default` branch, add one — both are open by design
13
+ (CONSTITUTION R1) so future provider values arrive additively instead of breaking your build. The
14
+ only changed signature is `OpenAITranscriptionAdapter.transcribe()`, which now returns an object;
15
+ read `.text`. The `transcribe()` helper is unaffected. Full detail: [MIGRATION.md](./MIGRATION.md).
16
+
17
+ ### Changed — packaging (install/runtime level; no source change for consumers)
18
+
19
+ - **Node floor raised to `>=22`** (was `>=18`). Node 18 and 20 are both end-of-life; 22 is also the
20
+ floor `openai-node` 7 adopted.
21
+ - **`tiktoken` is now an OPTIONAL PEER dependency**, not an `optionalDependency`.
22
+ `optionalDependencies` means *"do not fail the install if this package fails to build"* — npm
23
+ installs it regardless, so every consumer received its ~5.6 MB wasm file. **If you use exact
24
+ OpenAI token counting, run `npm i tiktoken`**; the error thrown when it is missing names the
25
+ package and the alternatives (count-API and heuristic counters need no extra packages).
26
+ - **The wasm no longer lands in consumer bundles.** The dynamic import used a string literal, which
27
+ every bundler resolves during module-graph construction — so the blob was emitted even for code
28
+ paths that were never reached (one consumer reported it as **88% of their production output**).
29
+ `sideEffects: false` cannot prevent this: emitting a dynamic-import chunk is a graph-resolution
30
+ outcome, not dead-code elimination. The specifier now lives in a variable, opaque to bundlers and
31
+ resolved identically at runtime. Verified with a control — a literal import emits a 5.3 MB
32
+ `.wasm`; the shipped build emits none, even when the consumer imports `ContextMeasurer` directly
33
+ and has `tiktoken` installed.
34
+ - **`tiktoken` still works in the browser.** It ships a wasm/ESM build that bundlers resolve for
35
+ browser targets, so it is deliberately *not* stubbed out of `index.browser.js`.
36
+ - **`HybridTokenCounter` builds its tiktoken counter lazily**, on first route to that strategy,
37
+ rather than in the constructor.
38
+ - **The "zero dependencies" claim is now qualified** as *zero **required** runtime dependencies*.
39
+ `dependencies` genuinely is empty, but a consumer reading only that field concluded there were no
40
+ runtime packages at all.
41
+
42
+ ### Added — MCP speaks both protocol eras
43
+
44
+ `mcp` 2.0.0 shipped the **2026-07-28 revision**, which is not additive: it deletes the `initialize`
45
+ handshake, the session id, and the whole server→client back-channel. Real servers are still almost
46
+ entirely on 2025-11-25, so this is **dual-era or it is a regression** — both wires are supported and
47
+ neither is preferred.
48
+
49
+ **Verified against a real server, on every transport.** The modern wire was developed against our
50
+ own test doubles, which is not evidence: a shape that satisfies a fake can still be rejected by a
51
+ real implementation. Before release the whole surface was run against the official
52
+ `mcp` 2.0.0 Python server — negotiation, tools, resources, prompts, MRTR, `subscriptions/listen`
53
+ with live change events, result caching and era gating — over **stdio, Streamable HTTP and
54
+ WebSocket**, plus a real 2025-11-25 server (DeepWiki) to prove the fallback. That exercise found
55
+ six defects that no unit test could have caught, four of which failed *silently*: the per-request
56
+ `_meta` identity envelope was missing (on ordinary requests, and separately on the long-lived
57
+ `subscriptions/listen` POST, where the rejection surfaced as the stream simply ending — so `listen()`
58
+ returned a subscription that looked alive and delivered nothing); `connectMcp` never forwarded
59
+ `cacheResults`, making the opt-in cache a no-op; the discover probe omitted the
60
+ `MCP-Protocol-Version` header that modern servers route on; the routing headers keyed off an era
61
+ that is not yet set during the probe; and the long-lived POST accepted only `text/event-stream`,
62
+ which a modern server answers with `406`. All are fixed and pinned by regression tests.
63
+
64
+ - **`server/discover` negotiation with handshake fallback.** `ConnectMcpOptions.protocolMode`:
65
+ `'auto'` (default) probes the modern wire and falls back to `initialize`; `'legacy'` skips the
66
+ probe entirely (byte-identical to 1.x); a version string adopts that revision directly.
67
+ - **The fallback is a denylist, not an allowlist.** *Every* JSON-RPC error falls back to the
68
+ handshake, except a `-32022` whose `supported` list is modern-only and shares nothing with us —
69
+ a genuine incompatibility that must surface rather than be papered over. **Transport and network
70
+ errors are never treated as an era verdict**: silently downgrading the wire because a socket
71
+ blipped would be the worst available failure mode.
72
+ - **`McpClient.info` is unchanged on both wires.** A modern server has no `initialize` result, so
73
+ one is synthesised from the discover result and its `_meta` server-info stamp. Callers never
74
+ branch on the era (CONSTITUTION.md R2 — absorb the difference, never expose a union). The stamp is
75
+ display-only per spec, so absent *or* malformed degrades to a placeholder instead of failing the
76
+ connection.
77
+ - **New (additive):** `McpClient.protocolVersion`, `.era`, `.discoverResult`, the
78
+ `McpDiscoverResult` type, and the version registry (`MCP_KNOWN_PROTOCOL_VERSIONS`,
79
+ `mcpEraOf`, …). Versions are treated as an **enumerated set, not an ordered scalar** — comparing
80
+ `'zzz' > '2025-11-25'` is true and meaningless, so era questions go through the registry.
81
+ - **Methods the revision removed are gated by era.** `logging/setLevel` and `resources/subscribe`
82
+ throw on a modern session with a message naming the negotiated version and the replacement,
83
+ instead of letting the server answer a bare `-32601`. The `ping` keep-alive is not started on a
84
+ modern session. All of them are untouched on a handshake session.
85
+ - **A 4xx carrying a JSON-RPC error body no longer loses it.** The HTTP transport collapsed every
86
+ 4xx into `ConnectionClosed`, which discarded exactly the `-32022` that negotiation depends on — a
87
+ modern-only server looked like a dead connection.
88
+ - New error codes: `HeaderMismatch` (-32020), `MissingRequiredClientCapability` (-32021),
89
+ `UnsupportedProtocolVersion` (-32022).
90
+
91
+ **Multi-round-trip requests (MRTR, SEP-2322)** — the modern replacement for the back-channel. Where
92
+ a handshake-era server *pushes* a `sampling/createMessage` at us mid-call, a 2026-07-28 server
93
+ *returns* `resultType: 'input_required'` with the questions it needs answered, and the client
94
+ re-issues the same call carrying the answers plus the server's opaque `requestState`.
95
+
96
+ - **One handler serves both wires.** MRTR is dispatched through the same `onServerRequest` path as
97
+ a pushed request, so a caller who wired up sampling/elicitation/roots once gets it on either wire
98
+ without knowing which is in play.
99
+ - **`McpCallResult` did not become a union.** Upstream models this as a separate
100
+ `InputRequiredResult`, which would break every consumer reading `.content`. We attach
101
+ `resultType` / `inputRequests` / `requestState` as optional fields instead (CONSTITUTION.md R2).
102
+ An **absent `resultType` reads as `'complete'`**, so every pre-2026 result behaves exactly as
103
+ before and costs no extra round-trip.
104
+ - Applied to `tools/call`, `prompts/get` and `resources/read`. `requestState` is echoed back
105
+ byte-exact and never inspected.
106
+ - A leg carrying state but no questions backs off (50 ms doubling to a 250 ms cap, reset by any leg
107
+ with real questions) rather than spinning against the server.
108
+ - `inputRequiredMaxRounds` (default **10**, matching the other SDKs) bounds the loop, because a
109
+ handler that never satisfies the server would otherwise retry forever.
110
+
111
+ **`subscriptions/listen` (SEP-2575)** — the single change-notification stream that replaces
112
+ `resources/subscribe` and the standalone notification channel at 2026-07-28.
113
+
114
+ - `McpClient.listen(filter, onEvent)` returns an `McpSubscription`. Every kind is **opt-in**
115
+ (`toolsListChanged`, `promptsListChanged`, `resourcesListChanged`, `resourceSubscriptions[]`) and
116
+ the server acknowledges with the subset it actually honoured — which **can be narrower than what
117
+ was requested**, so `subscription.honored` / `isHonored(kind)` is worth checking rather than
118
+ assuming. Frames are attributed by the `io.modelcontextprotocol/subscriptionId` stamp, so frames
119
+ for another subscription are ignored.
120
+ - Refused with a clear error on a handshake session, which keeps `subscribeResource()` the right
121
+ answer there instead of silently returning a subscription that never fires.
122
+ - **Works on every transport** — stdio, WebSocket and Streamable HTTP. On HTTP a listen is a POST
123
+ whose *response body* is the long-lived stream, so it goes through the streaming fetch rather than
124
+ the buffered POST path (which would surface frames only once the stream closed — i.e. never, for
125
+ a healthy subscription). Frames route identically on all three, so the client sees no difference.
126
+ - **The end of a stream is observable.** `subscription.active` / `.ended` report whether the stream
127
+ is still live and, if not, the error that killed it — a rejected subscription, a dropped
128
+ connection, or a clean server-side teardown. A subscription that silently stopped delivering is
129
+ otherwise indistinguishable from one where nothing has changed yet; failures also surface on the
130
+ `onMcpError` hook. `close()` on the client tears down every open stream.
131
+
132
+ **Hardening**
133
+
134
+ - **The stdio read buffer is bounded** (`maxBufferSize`, default **10 MB**, matching mcp-ts 1.30).
135
+ JSON-RPC over stdio is newline-delimited, so a server that never emits `\n` — a crash dump, a
136
+ binary blob on the wrong stream, a runaway log line — grew the buffer until the process died.
137
+ The limit applies to a single *unterminated* line, so a large burst of complete messages is
138
+ unaffected; on overflow the pending requests fail with a message naming the likely cause.
139
+ - **`Content-Type` is compared by media-type essence, not substring.** `contentType.includes(...)`
140
+ routed anything merely *containing* `text/event-stream` — e.g. `application/json;
141
+ profile="text/event-stream"` — into the SSE parser.
142
+ - **RFC 9207 `iss` validation** on the OAuth authorization response, checked **before** the code is
143
+ redeemed. This is the mix-up-attack defence: without it a malicious authorization server can hand
144
+ back a code minted by a different server and have the client replay the user's credentials
145
+ against it. Comparison is exact string equality per §2.4 — deliberately *not* URL-normalised,
146
+ since that leniency is what an attacker looks for. A **missing** `iss` is rejected when the server
147
+ advertises `authorization_response_iss_parameter_supported`, otherwise stripping the parameter
148
+ would skip the check. Pass it via `finishMcpAuth(..., { iss })`; optional, so existing callers
149
+ keep working.
150
+ - **`application_type: 'native'` is sent at dynamic client registration** (SEP-837). MCP clients are
151
+ normally local processes with a loopback redirect, and some authorization servers apply stricter
152
+ redirect-URI rules when the type is left to be guessed as `web`. An explicit value from the caller
153
+ still wins.
154
+ - **The WebSocket transport is kept** and now documents itself as non-standard. Upstream removed
155
+ theirs as "never part of the MCP specification"; ours is public API we shipped, and an upstream
156
+ deletion is not our deletion (R7). It is also duplex, so it supports `subscriptions/listen` today.
157
+
158
+ **Result cache hints (`ttlMs` / `cacheScope`)** — opt-in via `cacheResults`, off by default.
159
+ Settable on `connectMcp(config, { cacheResults: true })` as well as on `McpClient` directly, along
160
+ with `inputRequiredMaxRounds`.
161
+
162
+ - Honours the server's freshness hint on `tools/list`, `prompts/list`, `resources/list`,
163
+ `resources/templates/list` and `resources/read`. **A server that sends no hints caches nothing**,
164
+ so this is a no-op against every pre-2026 server.
165
+ - **`ttlMs: 0` means "immediately stale"** — a real instruction, not a missing value to be replaced
166
+ with a default.
167
+ - A paginated list is only as fresh as its shortest-lived page, so the effective TTL is the
168
+ **minimum** across pages.
169
+ - Entries are dropped on the matching `*_changed` notification **before** the caller's handler
170
+ runs, so a handler that re-lists synchronously never reads a stale entry. A
171
+ `notifications/resources/updated` drops only the named resource.
172
+ - `cacheScope` is recorded but never used to widen sharing: this cache lives inside one client with
173
+ one credential, where `public` buys nothing.
174
+
175
+ ### Changed — `FinishReason` is now an OPEN union
176
+
177
+ - **`FinishReason` = `KnownFinishReason | (string & {})`.** Providers keep inventing terminal
178
+ states — four did so in a single upstream cycle — and against a closed union every one of those is
179
+ a breaking change for **every** consumer, including consumers of providers that changed nothing.
180
+ Opening it is what CONSTITUTION.md R1 exists for, and it means this is the **last** time this type
181
+ breaks anyone. Write a `default` branch; use `KnownFinishReason` for the documented set alone.
182
+ - **New known value `'malformed_tool_call'`** — the model tried to call a tool and produced
183
+ something unusable. Distinct from `error` (the request failed) and `tool_use` (a call we can
184
+ run), because this one is *recoverable*.
185
+ - **Google's `MALFORMED_FUNCTION_CALL` is now mapped.** It previously wasn't mapped at all, so it
186
+ fell through to `'stop'`: a turn where the model failed to produce a usable tool call looked like
187
+ a clean finish with no content.
188
+
189
+ ### Added — agent + network
190
+
191
+ - **`reflectAndRetry` on `AgentLoop`** (google-adk 2.6 `ReflectAndRetryModelPlugin`).
192
+ Self-healing recovery from a model failure the model itself can fix: it receives structured
193
+ guidance naming the attempt and forbidding an identical retry, then the step runs again within a
194
+ bounded budget. **Off unless configured** — a retry costs a real request.
195
+ - **Not a network retry.** The engine already retries transport failures; this is for a request
196
+ that *succeeded* and came back unusable, which resending unchanged would never fix.
197
+ - The failed turn is **not appended to history**, so the model never learns from its own broken
198
+ output; usage from it *is* counted, because a wasted turn still costs money.
199
+ - Counts **consecutive** failures, so an agent that recovers and fails again later gets a fresh
200
+ budget rather than inheriting a spent one.
201
+ - `throwIfExceeded` (default `true`) decides raise-vs-return when the budget is spent; the error
202
+ names the option so the alternative is discoverable.
203
+
204
+ - **`checkProvenance()`** — detect provider provenance signals in a file (C2PA manifest, SynthID
205
+ watermark) via OpenAI's new `POST /v1/content_provenance_checks`. Bytes in, structured verdict
206
+ out, same shape as `moderate()`, with an honest-zero cost entry so the ledger records the call.
207
+ It is the only "was this AI-generated" primitive any tracked SDK ships.
208
+ - The result separates **`detected`** from **`trusted`**, and the docs say why: signals are
209
+ strippable — a re-encode, crop or screenshot usually removes them — so `detected: false` is
210
+ absence of evidence, not evidence a human made the file. Only a detected manifest that
211
+ *validated* is a positive statement.
212
+ - `detected` is true if ANY signal fired: audio carries SynthID only, so requiring both schemes
213
+ would report every audio file as clean.
214
+ - **`AnchoredStrategy`** for ContextGuard — one growing scratchpad instead of a chain of summaries
215
+ (ported from google-adk 1.5 `AnchoredContextCompactor`). `LayeredStrategy` emits a new summary
216
+ per compaction, so old facts get summarised repeatedly and drift; anchored merges each compaction
217
+ into a single head entry, so every fact is summarised from raw text exactly once. The trade is
218
+ stated in the file: one anchor means one blast radius.
219
+ - **Never splits a tool call from its result.** The retain boundary walks backwards past a
220
+ `tool_result` whose `tool_call` would be cut away — several providers reject an orphaned
221
+ result outright and the rest silently misread it.
222
+ - **A summariser returning nothing declines rather than dropping entries**: trading a context
223
+ overflow for silent data loss is strictly worse.
224
+
225
+ - **`toolNameCollisionPolicy`** on `AgentLoop` (`'warn'` default, `'error'`). Tools are registered
226
+ in a map keyed by function name / builtin type, so two tools sharing a key meant one **silently
227
+ replaced** the other and the model never saw it — surfacing much later as "the model called the
228
+ wrong tool", with nothing in the logs pointing at the cause. `'warn'` keeps last-write-wins (an
229
+ app relying on a deliberate override still works — R4) but emits an `onWarning`
230
+ (`code: 'tool_name_collision'`) naming the key and which tool lost; `'error'` throws at
231
+ construction or `addTool()`, before the model is called. Re-registering the *same* tool object is
232
+ not a collision.
233
+ - **Per-request retry overrides** — `HttpRequest.retry` (`maxRetries`, `totalTimeoutMs`,
234
+ `attemptTimeoutMs`, `maxRetryAfterMs`, `backoff`). A queue's retry policy is shared by every call
235
+ on it, so a one-off that needs to be more or less patient — a long batch submit, a health check
236
+ that should fail fast — previously had to accept the shared policy or get its own queue. Mirrors
237
+ Google moving `retryOptions` from client-level to per-request `HttpOptions` (google-ts 2.15).
238
+ `perKind` deliberately stays queue-level: one request cannot redefine which error classes are
239
+ retryable for everyone sharing the queue. Precedence is per-request → per-kind → queue default.
240
+
241
+ ### Added — OpenAI Responses parity
242
+
243
+ - **Assistant `phase` (`commentary` | `final_answer`)** on `TextPart` and on streamed text events.
244
+ Codex-family models narrate before answering; `response.text` concatenates both, so **an agent's
245
+ final output used to include its own thinking-out-loud**. `AgentLoop` now derives its answer with
246
+ the new `finalAnswerText()` helper, which drops commentary. `response.text` and `contentText()`
247
+ are unchanged — callers who want everything still get everything.
248
+ - Open union (R1), and `finalAnswerText` excludes only what is explicitly `'commentary'` rather
249
+ than keeping only `'final_answer'`: the day a provider adds a third phase, an allow-list would
250
+ silently drop the answer.
251
+ - Streaming carries it too. `phase` is announced once on `response.output_item.added` and belongs
252
+ on every delta of that item, so the parser keeps per-stream item→phase state; concurrent streams
253
+ cannot leak phases into each other. Commentary is still yielded to the consumer (a UI may well
254
+ want to render it live) and is preserved in the assembled content as its own phase-tagged part.
255
+ - Nothing is inferred: a model that reports no phase produces parts with no phase, exactly as
256
+ before.
257
+ - **`name` + `namespace` on `function_call_output`.** The tool name is taken from the matching
258
+ call — tracked across messages while building the input, never invented, so a result with no
259
+ matching call simply omits it. `ToolResultPart.namespace` round-trips the namespace of a
260
+ namespaced tool. Probe-verified 2026-08-06: accepted, and a non-string `namespace` is rejected,
261
+ so the fields are validated rather than tolerated.
262
+
263
+ ### Added — programmatic tool calling (the model writes code that calls your tools)
264
+
265
+ A model can now write a short program that orchestrates your tools itself, instead of
266
+ emitting one call at a time and waiting for each result. Previously the `program` items
267
+ in the response were dropped on the floor.
268
+
269
+ - **`ToolCaller` on `ToolCallPart` and `ToolResultPart`** — `{ type: 'direct' | 'program',
270
+ callerId? }`. Absent means what it always meant (the model called the tool itself), so
271
+ nothing changes for existing code. Open union with an optional payload rather than
272
+ `{type:'direct'} | {type:'program', callerId}` (R1 + R2), so a future caller kind is
273
+ additive; an unknown type is preserved rather than flattened to `direct`.
274
+ - **`ProgramCallPart` (`program_call`) and `ProgramResultPart` (`program_result`)** —
275
+ the code the model wrote, and what it returned. The code is plain readable JavaScript
276
+ and worth surfacing: it is the plan the model is executing.
277
+ - **`allowedCallers` is now enforced locally**, not only by the provider. A tool without
278
+ it is `direct`-only, so model-written code cannot reach a tool that never opted in. A
279
+ violation denies that one call with an error result to the model — the way a guardrail
280
+ trip does — rather than ending the run.
281
+ - **Round-tripping is the whole feature**, and three wire rules make it work (all found
282
+ live, none of them in any SDK's types):
283
+ - The `program` item is **rejected without the `reasoning` item that produced it**, so
284
+ that item is captured and re-emitted with it.
285
+ - **Dropping the program item is worse than an error**: the model silently re-emits the
286
+ program and runs it again from the start.
287
+ - `program_output` **requires its `id`** when replayed as history — unlike
288
+ `function_call_output`, which needs none. Without it a follow-up question 400s on a
289
+ conversation that had just succeeded.
290
+ - **Availability, checked model by model:** of the 53 gpt-5 / o3 / o4 / codex models
291
+ visible on the test account, only the **`gpt-5.6` family** (`luna`, `sol`, `terra`)
292
+ accepts the `programmatic_tool_calling` tool. Every other one returns
293
+ *"Tool 'programmatic_tool_calling' is not supported with <model>"*.
294
+
295
+ ### Added — structured transcription
296
+
297
+ `transcribe()` returned `{ text }` and nothing else, so segments, speakers and word timings that
298
+ the provider had already computed were parsed and thrown away.
299
+
300
+ - **New request options:** `keywords` (spelling control for names and jargon), `languages`
301
+ (candidate languages when the language is unknown), `wordTimestamps`, and `diarization`.
302
+ - **New response fields, all optional:** `segments` (with `speaker` when diarizing), `words`,
303
+ detected `languages`, and `durationSeconds`. **`text` stays required** (R3), so existing code is
304
+ untouched — the additions appear only when the chosen model produces them.
305
+ - **Behaviourally verified, not just accepted** (E2). `keywords` changes the transcript: an invented
306
+ name that comes back as *"Zalbrequist"* without it comes back as *"Zylberquist"* with it, on
307
+ identical audio. `languages` changes what the model reports detecting, and an invalid code is
308
+ rejected. Both are `gpt-transcribe`-only; word timings are `whisper-1`-only; speaker labels are
309
+ `gpt-4o-transcribe-diarize`-only. **No model returns speakers and word timings together**, so
310
+ combining `wordTimestamps` with `diarization` throws before any request is sent.
311
+ - **Model-gated options are still sent.** A field the chosen model rejects produces a 400 naming
312
+ the parameter, rather than being dropped on our side — the caller learns their keywords did
313
+ nothing (R4: gating is internal, but silence is not a gate). On generateContent providers, which
314
+ have no structured endpoint at all, the same request emits an `onWarning`
315
+ (`transcription_option_unsupported`).
316
+ - **Transcription cost is now measured, not estimated.** These models return the audio duration
317
+ they billed for (`usage.seconds`, or a top-level `duration`), which is used when the caller
318
+ supplies none. Previously a non-WAV file with no `audioDurationSeconds` could only produce an
319
+ honest zero.
320
+ - **Google's equivalent is deliberately absent.** `audioTranscriptionConfig` is accepted *and
321
+ type-validated* by the Gemini Developer API and then completely ignored: a two-speaker round-trip
322
+ returned a response structurally identical to the control — no `speakerLabel`, no `words[]`
323
+ anywhere (2026-08-09). It is the second confirmed accepted-but-inert field after `top_k`. Shipping
324
+ it on the strength of the green probe would have meant a diarization feature that silently
325
+ returns nothing.
326
+
327
+ **Breaking (2.0):** `OpenAITranscriptionAdapter.transcribe()` returns
328
+ `OpenAITranscriptionResult` instead of `string`; read `.text`. The `transcribe()` helper is
329
+ unaffected — it already returned an object.
330
+
331
+ ### Fixed — correctness
332
+
333
+ - **Parallel tool calls were broken on every chat-completions backend.** The loop answers a round
334
+ of parallel calls with one tool message holding a `tool_result` part per call, and this API wants
335
+ a separate `{role:'tool'}` message per `tool_call_id` — but only the **first** was emitted. Every
336
+ call after the first went unanswered and the provider rejected the whole request with
337
+ *"No tool output found for function call <id>"*. Affected OpenRouter and any use of
338
+ `api: 'completions'` on OpenAI/xAI; the Responses path was always correct. Present in 1.7.0 and
339
+ earlier; found by running the examples corpus, not by a unit test.
340
+ - **`serviceTier: 'fast'` is actually sent to OpenAI.** The value shipped in openai-ts 7.x but was
341
+ missing from our known-tier set, so `openaiRequestTier('fast')` fell through to `'auto'` — a
342
+ caller asking for Fast mode silently got the project default, with no error and no warning.
343
+ Probe-verified on `gpt-5.5`: `fast` accepted, `hyperfast` rejected, so the value is validated
344
+ rather than merely tolerated. Applies to Responses and chat-completions.
345
+ - **A `Retry-After` longer than we will honour now fails fast instead of parking the request.** New
346
+ config `RetryConfig.maxRetryAfterMs` (default **120s**). Previously an un-capped value was obeyed
347
+ verbatim: `Retry-After: 86400` held the request for a day, which from the caller's side is
348
+ indistinguishable from a hang. Worse, on the rate-limit path it also paused the **entire**
349
+ limiter — every request on that queue, not just the one that was throttled. Both paths are now
350
+ clamped, and an over-cap value is treated as a refusal rather than a delay.
351
+ - **`Retry-After` parsing hardened.** The HTTP-date form (RFC 9110) is now parsed instead of being
352
+ silently ignored, and malformed values (`NaN`, negative, non-finite, a past date) are discarded
353
+ rather than propagated — `setTimeout(fn, NaN)` fires immediately, which turned one bad header
354
+ into an instant retry storm.
355
+
356
+ ### Unchanged, deliberately
357
+
358
+ - **Google Interactions keeps sending `temperature` and `top_p`.** google 2.15 deleted both from its
359
+ Interactions `GenerationConfig` type, which resembles the pattern behind two earlier live
360
+ breakages — but the wire disagrees: probed 2026-08-06 on `gemini-3.6-flash`, both are accepted
361
+ (200) and *validated* (`"warm"` / `-7` → 400). The removal is SDK-typing-only; stripping them
362
+ would have been the regression. Recorded at the call site so a later cycle does not "fix" it.
363
+
364
+ ---
365
+
366
+ Upstream reconciliation for the 2026-07-27 clone refresh (10 SDKs). This batch is dominated by
367
+ **terminal-state correctness**: three providers widened response enums that our adapters silently
368
+ flattened to `'stop'`, so a caller could not distinguish a refusal, a context overflow, a queued
369
+ interaction or an outright failure from a clean finish.
370
+
371
+ ### Fixed
372
+ - **Google Interactions no longer sends `cached_content`.** google 2.13 removed it from the
373
+ Interactions request model and the endpoint now hard-rejects it — live-probed:
374
+ `400 Unknown parameter 'cached_content'`. Any call passing `providerOptions.cachedContent` on
375
+ Interactions failed outright. The passthrough **moved to `generateContent`**, which still accepts
376
+ and validates it (top-level `cachedContent`), so the capability is preserved rather than dropped.
377
+ - **Anthropic `refusal` → `finishReason: 'content_filter'`** (was `'stop'`). A safety decline is a
378
+ block, not a clean finish; it now lines up with every other provider's block signal. The refusal
379
+ category enum also gained `general_harms` (anthropic 0.115).
380
+ - **Anthropic `model_context_window_exceeded` → `finishReason: 'length'`** (was `'stop'`), on both
381
+ the buffered and streamed reason maps.
382
+ - **OpenAI Responses `status: 'failed'` → `finishReason: 'error'`** (was `'stop'`), and `cancelled`
383
+ → `'error'`. A Responses call can fail *inside a 200*, so there was no exception to catch and the
384
+ caller silently received an empty success.
385
+ - **Google Interactions `queued` no longer ends a stream.** The status is non-terminal, but the
386
+ stream parser emitted a terminal `done` for it, truncating the run.
387
+
388
+ ### Security
389
+ - **`TelemetryAdapter` can redact provider error text.** New option
390
+ `includeSensitiveData` (default `true` — unchanged behaviour, and the same default as the OpenAI
391
+ Agents SDK's `trace_include_sensitive_data`). A provider's `error.message`/`error.raw` can echo
392
+ request content back (a refusal quotes the prompt, a validation error names the field and value),
393
+ and we stored it verbatim. With `includeSensitiveData: false` the message becomes `***REDACTED***`
394
+ and `raw` is dropped, while `name`/`code`/`status` are kept so traces stay triageable. URLs and
395
+ headers were, and remain, always redacted.
396
+
397
+ ### Fixed (hardening)
398
+ - **SSE streams are now cancelled, not just unlocked.** `parseSSEStream` released the reader lock in
399
+ its `finally` but never cancelled the body, so a consumer that broke out early (abort, error, or a
400
+ `break` after the first token) left the HTTP response open until GC. Verified live on Anthropic,
401
+ OpenAI and Google: full streams unchanged, early `break` tears down cleanly.
402
+ - **Non-replayable request bodies are never retried.** A streamed body is consumed by the first
403
+ attempt, so a retry would send an empty/partial body. Our own `rawBody` callers all pass FormData
404
+ or bytes (replayable), so this is a guard against a caller-supplied stream rather than a live bug.
405
+ - **Case-insensitive response-header lookup.** `google/files.ts` guessed three casings of
406
+ `x-goog-upload-url` and would have missed any fourth. Header reads now go through one shared
407
+ `header()` helper in `util/http` (de-duplicated with the private copy in `llm/files/retrieve.ts`).
408
+
409
+ ### Added
410
+ - **`topK` and `seed` sampling options.** Both were reachable on several providers and exposed by
411
+ none of our surface — parity gaps found by the new feature-matrix audit and closed the same day.
412
+ Each is emitted **only where the wire accepts it**, verified by live probe rather than inferred:
413
+ `topK` → Anthropic, Google (generateContent *and* Interactions), xAI chat, OpenRouter chat;
414
+ dropped for OpenAI, which defines no top-k. `seed` → OpenAI **chat-completions**, Google (both
415
+ surfaces), xAI (chat *and* responses), OpenRouter chat; dropped for Anthropic and OpenAI
416
+ **Responses**, which both reject it (400). Sending either to a surface that refuses it is a hard
417
+ error, so the gating is locked by unit tests and was live-verified end to end.
418
+ - **`docs/feature-matrix.json` — the parity matrix.** Every capability an official SDK exposes, how
419
+ each provider spells it, and where we stand, with citations into the version-pinned clones.
420
+ `scripts/validate-feature-matrix.mjs` runs as part of `bun run lint` and fails the build on a
421
+ broken citation, an unexplained `partial`/`beta`/`by-design`, or a duplicate id. It backs the
422
+ site's comparison page and is maintained by the upstream-update cycle.
423
+ - **`FinishReason` gains `'pending'`** — non-terminal: the provider accepted the request but has not
424
+ produced a completion (Google Interactions `queued`, OpenAI Responses `queued`/`in_progress` in
425
+ background mode). Treat as "poll/retry", never as a result. *Additive union member: exhaustive
426
+ `switch` statements over `FinishReason` should add a case.*
427
+ - **`CompletionResponse.error?: { code?, message? }`** — populated when `finishReason === 'error'`,
428
+ carrying the provider's own failure detail (e.g. OpenAI's new `data_residency_mismatch` code,
429
+ openai 6.49). Optional field; absent unless the provider reported a failure.
430
+ - Documented the OpenAI `reasoning.context` default (the `gpt-5.6` family defaults to `all_turns`,
431
+ earlier models to `current_turn`).
432
+ - **`itemId` on `text` / `thinking` stream events.** OpenAI Responses reports which output item a
433
+ delta belongs to (`item_id`); a turn can interleave deltas from several items, so consumers that
434
+ reassemble per item — rather than concatenating into one string — now can. Optional and additive:
435
+ ignoring it gives exactly the previous behaviour, and providers that report no item id (chat
436
+ completions) simply omit it. Live-confirmed present on every delta from the Responses API.
437
+
438
+ ## [1.7.0] - 2026-07-16
439
+
440
+ ### Added
441
+ - **Video extend + edit (xAI grok-imagine-video).** `VideoGenRequest` gains `sourceVideo?: DataSource`
442
+ and `params.videoMode?: 'extend' | 'edit'`. When a source video is present the xAI adapter routes to
443
+ the right endpoint instead of plain generation: `extend` (default) → `POST /v1/videos/extensions`
444
+ (continues from the last frame; takes `duration`, ignores aspect/resolution), `edit` →
445
+ `POST /v1/videos/edits` (prompt + video only). The clip is passed as a public URL, a Files-API id, or
446
+ an inline base64 data-URL. `MediaCapabilities` gains `videoExtension`; `MediaOutput.generateVideo`
447
+ throws if a `sourceVideo` is sent to a provider that doesn't support it (rather than silently
448
+ generating). `generateVideo(req)` is unchanged — the new fields flow through. Verified live end-to-end
449
+ on `grok-imagine-video`: generate → extend → edit all return video bytes. (Note: extend/edit require
450
+ `grok-imagine-video`, not `grok-imagine-video-1.5`, which is generation-only.)
451
+ - **`onMediaProgress` hook.** Long-running async video ops (generate/extend/edit) now emit an
452
+ `onMediaProgress` event once per poll (`{ type, provider, operationId, progress, model }`), so a UI can
453
+ render a progress bar. Progress values confirmed live (0→100).
454
+ - **`RawMediaResult.sourceUrl` + `MediaMeta.sourceUrl`.** Async video results now carry the provider's
455
+ hosted URL, so callers can render or re-submit the asset without holding the bytes.
456
+ - **Unified reasoning visibility.** `ThinkingConfig` gains `visibility: 'full' (default) | 'summary' |
457
+ 'hidden'`, mapping to Anthropic `enabled.display`, OpenAI Responses `summary`, and Google
458
+ `includeThoughts` — one knob for "how much reasoning comes back" across providers (best-effort; a
459
+ provider without a middle state degrades `summary` to full). Default `full` = prior behaviour.
460
+ - **OpenAI reasoning execution mode.** `providerOptions.reasoningMode: 'standard' | 'pro'` maps to
461
+ `reasoning.mode` on the OpenAI **Responses** path (chat-completions rejects it). Kept in
462
+ `providerOptions` rather than a first-class knob since only one provider/API honours it.
463
+ - **Google `translationConfig` passthrough.** `providerOptions.translationConfig` forwards to
464
+ `generationConfig.translationConfig` on generateContent (Gemini Developer API; live-verified 200).
465
+ - **OpenAI native moderation blocking.** `providerOptions.moderationPolicy`
466
+ (`{ input?: { mode: 'score'|'block' }, output?: {…} }`) forwards to OpenAI's `moderation.policy` on
467
+ Responses + chat for server-side blocking. Unified moderation stays report-only by design (blocking is
468
+ `moderationGuardrail` at the agent layer); this is an OpenAI-specific opt-in, so it lives in
469
+ `providerOptions`. Live-verified the field is accepted.
470
+ - **OpenAI explicit prompt caching.** `providerOptions.promptCacheOptions`
471
+ (`{ mode: 'implicit'|'explicit', ttl: '30m' }`) forwards to `prompt_cache_options` (gpt-5.6+),
472
+ true-OpenAI only (xai/openrouter inherit the builder and don't emit it). Note: OpenAI caches
473
+ **implicitly by default**, so the unified `cache` config already works there with no config — this
474
+ passthrough is for manual control. (`prompt_cache_retention` is deprecated upstream in favour of
475
+ `prompt_cache_options.ttl`.)
476
+ - **OpenAI programmatic tool calling (Responses).** `BuiltinTool` gains `programmatic_tool_calling`, and
477
+ `FunctionTool` gains `allowedCallers?: ('direct'|'programmatic')[]` + `outputSchema?` — emitted on the
478
+ OpenAI Responses path only. Live-verified on gpt-5.6 (tool calls succeed; older models reject the
479
+ builtin, which is model-gated). (Surfacing the `program`/`program_output` output items is deferred; the
480
+ parser already tolerates them without error.)
481
+
482
+ ### Fixed
483
+ - **OpenAI prompt-cache write tokens were dropped.** Both usage parsers hardcoded `cacheWriteTokens: 0`;
484
+ they now read `input_tokens_details.cache_write_tokens` (Responses) / `prompt_tokens_details.
485
+ cache_write_tokens` (chat), so cost accounting no longer under-reports explicit prompt caching.
486
+ - **Google reasoning was live-broken (two paths).** generateContent sent `thinkingLevel`, which the
487
+ Gemini Developer API 400s on **2.5** models (it is 3.x-only) — now routed per series: 2.5 →
488
+ `thinkingBudget` (token count), 3.x → `thinkingLevel`. The Interactions path wrapped `thinking_config`
489
+ (rejected outright) and used uppercase values — it takes `thinking_level` **flat** on `generation_config`
490
+ and **lowercase** (`minimal/low/medium/high`). Both live-verified across gemini-2.5 + 3.5.
491
+ - **Google Interactions rejected sampling penalties.** We emitted `presence_penalty`/`frequency_penalty`
492
+ on the Interactions path, which the API 400s ("Unknown parameter") — upstream removed them from its
493
+ Interactions config. No longer emitted there (still valid on generateContent).
494
+ - **Streamed tool-call id collision on OpenAI-compatible backends.** The chat-completions stream parser
495
+ keyed tool-call fragments by `id ?? ''`, so parallel calls from backends that omit ids (LiteLLM/Bedrock,
496
+ some OpenRouter routes) merged into one. Fragments are now correlated by `index`, with a stable
497
+ `call_<uuid>` synthesized once per index when the backend omits ids.
498
+ - **`content_filter` finish reason was flattened to `stop`/`length`.** The chat-completions stream reason
499
+ map lacked `content_filter`, `AgentLoop` derived every normal-completion finish as `stop`, and the
500
+ Responses parser mapped `status: 'incomplete'` to `length` regardless of `incomplete_details.reason` —
501
+ all discarding the provider's actual reason. Now the stream map, the loop, and the Responses parser
502
+ (reading `incomplete_details.reason`) all surface `content_filter` (and `length`) to consumers — so a
503
+ moderation/safety block is distinguishable from a token cap. (Our loop already terminates on non-tool
504
+ finishes, so it never retry-looped on an empty filtered turn.)
505
+ - **Browser: xAI video result was unusable (CORS).** The generated clip lives on a cross-origin bucket
506
+ (`vidgen.x.ai`) that sends no `Access-Control-Allow-Origin`, so `downloadVideo`'s programmatic
507
+ byte-fetch was blocked in the browser and video generation failed outright. In the browser the adapter
508
+ now returns the hosted URL (via `sourceUrl`) with empty bytes instead of fetching — `<video src>` plays
509
+ it cross-origin without CORS, and it can be re-submitted as a `sourceVideo`. Node/Bun still download the
510
+ bytes. (Node-only tests couldn't surface this; CORS isn't enforced off-browser.)
511
+ - **Google `editImage` aspect ratio / size (same bug, second code path).** The 1.6.1 fix moved
512
+ `aspectRatio` / `imageSize` to `generationConfig.imageConfig` only in `generateImage`; the sibling
513
+ `editImage` method still wrote `generationConfig.responseFormat.image` and so 400'd on any edit that
514
+ passed an aspect ratio or size. Now both image paths use `imageConfig`. Verified live end-to-end:
515
+ generate → edit round-trip both return an image at `16:9` / `2K`. Locked with a unit regression on the
516
+ `editImage` request body.
517
+ - **xAI video generation polled forever after the job finished.** `getVideoStatus` only treated
518
+ `status: "completed"`/`"ready"` (or a `download_url`) as done, but xAI reports terminal success as
519
+ `status: "done"` with the URL under `video.url` — so a finished job kept polling until the wait cap and
520
+ never returned. `downloadVideo` likewise read `download_url`/`url` and missed `video.url` (and duration
521
+ under `video.duration`). Both now read the real `video.*` shape (flat fallbacks kept), and terminal
522
+ `status: "done"`/`"expired"` are handled. Progress is now carried on the processing status. Verified
523
+ live end-to-end (grok-imagine-video-1.5 image-to-video: progress 0→75→100 → downloaded 2.5 MB); locked
524
+ with a unit test replaying the real server payload.
8
525
 
9
526
  ## [1.6.1] - 2026-07-13
10
527
 
package/MIGRATION.md ADDED
@@ -0,0 +1,93 @@
1
+ # Migrating to 2.0.0
2
+
3
+ **Most codebases need no source changes.** The point of this library is that provider churn is our
4
+ problem to absorb, not yours — and a whole cycle of it (a new MCP protocol revision, a new OpenAI
5
+ major, four SDK majors) landed here without becoming a breaking change for you.
6
+
7
+ Three things can require action, and none of them is a provider change.
8
+
9
+ ## 1. Node 22+ is required
10
+
11
+ ```json
12
+ "engines": { "node": ">=22", "bun": ">=1.1.0" }
13
+ ```
14
+
15
+ Node 18 and 20 are both end-of-life. 22 is also the floor `openai-node` 7 adopted.
16
+
17
+ **What to do:** upgrade the runtime. Nothing in your code changes.
18
+
19
+ ## 2. `tiktoken` is now an optional PEER dependency
20
+
21
+ It used to be an `optionalDependency`, which means *"do not fail the install if this package fails
22
+ to build"* — npm installed it **anyway**. Every consumer received its ~5.6 MB wasm file, and
23
+ bundlers emitted it into production builds even when local token counting was never used. One
24
+ consumer measured it at **88% of their shipped output**.
25
+
26
+ **What to do:** if you use exact local OpenAI token counting, install it yourself:
27
+
28
+ ```sh
29
+ npm install tiktoken
30
+ ```
31
+
32
+ If you don't, do nothing — you now stop paying for a feature you never asked for. Token counting
33
+ still works without it: `countTokens` falls back to the provider count-API (Anthropic/Google) or a
34
+ calibrated heuristic. The error thrown when the package is genuinely needed names it and the
35
+ alternatives.
36
+
37
+ It still works in the **browser** when you do install it; it is deliberately not stubbed out.
38
+
39
+ ## 3. Two unions are now open — add a `default` branch
40
+
41
+ `FinishReason` and `ContentPart` gained members and are now open unions
42
+ (`KnownFinishReason | (string & {})`). If you `switch` over either **exhaustively, with no
43
+ `default`**, TypeScript will now complain.
44
+
45
+ ```ts
46
+ switch (res.finishReason) {
47
+ case 'stop': …
48
+ case 'tool_use': …
49
+ default: … // <- add this
50
+ }
51
+ ```
52
+
53
+ **This is deliberate, and it is the reason most of this release is not breaking.** Providers grew
54
+ four new terminal statuses in a single cycle. With a closed union, every one of those is a breaking
55
+ change for *every* consumer — including consumers of providers that changed nothing. Open unions
56
+ convert that into an additive change, at the cost of one `default` branch written once
57
+ (CONSTITUTION.md R1).
58
+
59
+ New members you can now handle if you want them: `'pending'` (queued / in-progress — previously
60
+ flattened to `'stop'`, claiming a clean finish for a response that had not run), and
61
+ `'malformed_tool_call'`. `ContentPart` gained `program_call` / `program_result`.
62
+
63
+ ## 4. One changed signature
64
+
65
+ `OpenAITranscriptionAdapter.transcribe()` returns `OpenAITranscriptionResult` instead of `string`:
66
+
67
+ ```ts
68
+ // before
69
+ const text = await adapter.transcribe(req, fetch);
70
+
71
+ // after
72
+ const { text } = await adapter.transcribe(req, fetch);
73
+ ```
74
+
75
+ The result also carries optional `segments`, `words`, `languages` and `durationSeconds`.
76
+
77
+ **The `transcribe()` helper is unaffected** — it already returned an object, and `text` is still
78
+ required on it. Only the low-level adapter class changed.
79
+
80
+ ## What did NOT break
81
+
82
+ Worth stating, because it is the whole design goal:
83
+
84
+ - **MCP 2025-11-25 keeps working, untouched.** The 2026-07-28 revision deletes the `initialize`
85
+ handshake, the session id and the entire back-channel — but this client speaks **both** wires and
86
+ prefers neither. No legacy path was removed. Even the WebSocket transport stays, documented as
87
+ non-standard, though upstream deleted theirs.
88
+ - **Every response type only gained optional fields.** Nothing was removed, narrowed, or made
89
+ required.
90
+ - **Every request option is still accepted.** Where a provider stopped taking one, we decide
91
+ internally whether it reaches the wire — your build does not break because of their typings.
92
+
93
+ Full detail in [CHANGELOG.md](./CHANGELOG.md).