@combycode/llm-sdk 3.1.0 → 3.3.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 +282 -0
- package/dist/agent/loop-step-state.d.ts +5 -1
- package/dist/catalog/catalog.d.ts +12 -0
- package/dist/index.browser.js +3980 -1165
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3980 -1165
- package/dist/llm/client-internal.d.ts +3 -1
- package/dist/llm/client.d.ts +10 -0
- package/dist/llm/providers/_shared/citations.d.ts +25 -0
- package/dist/llm/providers/anthropic/messages.d.ts +32 -7
- package/dist/llm/providers/anthropic/response-registry.d.ts +2 -0
- package/dist/llm/providers/anthropic/stream-registry.d.ts +2 -0
- package/dist/llm/providers/google/generate.d.ts +7 -8
- package/dist/llm/providers/google/interactions-registry.d.ts +2 -0
- package/dist/llm/providers/google/interactions-stream-registry.d.ts +2 -0
- package/dist/llm/providers/google/interactions.d.ts +7 -5
- package/dist/llm/providers/google/response-registry.d.ts +2 -0
- package/dist/llm/providers/google/stream-registry.d.ts +2 -0
- package/dist/llm/providers/openai/completions.d.ts +21 -3
- package/dist/llm/providers/openai/response-registry.d.ts +2 -0
- package/dist/llm/providers/openai/responses-registry.d.ts +2 -0
- package/dist/llm/providers/openai/responses-stream-registry.d.ts +2 -0
- package/dist/llm/providers/openai/responses.d.ts +31 -3
- package/dist/llm/providers/openai/stream-registry.d.ts +2 -0
- package/dist/llm/providers/openrouter/completions.d.ts +11 -7
- package/dist/llm/providers/openrouter/response-registry.d.ts +2 -0
- package/dist/llm/providers/openrouter/stream-registry.d.ts +2 -0
- package/dist/llm/providers/response-registries.d.ts +5 -0
- package/dist/llm/providers/xai/completions.d.ts +1 -4
- package/dist/llm/providers/xai/responses-registry.d.ts +2 -0
- package/dist/llm/providers/xai/responses.d.ts +12 -0
- package/dist/llm/providers/xai/stream-registry.d.ts +2 -0
- package/dist/llm/types/response.d.ts +23 -0
- package/dist/llm/types/stream.d.ts +14 -1
- package/dist/plugins/context-guard/facts.d.ts +9 -0
- package/dist/plugins/context-guard/tools.d.ts +3 -0
- package/dist/plugins/context-guard/types.d.ts +5 -0
- package/dist/util/audio-mime.d.ts +16 -0
- package/dist/util/compare.d.ts +13 -0
- package/dist/wire/interpreter.d.ts +2 -0
- package/dist/wire/response-interpreter.d.ts +156 -0
- package/dist/wire/response-specs.d.ts +7 -0
- package/dist/wire/stream-interpreter.d.ts +94 -0
- package/dist/wire/stream-specs.d.ts +8 -0
- package/package.json +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,288 @@ All notable changes to `@combycode/llm-sdk` are documented here. The format foll
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [3.3.0] - 2026-09-06
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **xAI's batch API had never worked, and could not say so.** Three readings were wrong. The
|
|
14
|
+
status counts live under `state`, not at the top level, so `total` was always 0, the job never
|
|
15
|
+
reached a terminal state, and a polling caller waited on a batch that had completed in seconds --
|
|
16
|
+
no error, no output, just a wait. The results are nested and TAGGED,
|
|
17
|
+
`batch_result.response.<variant>`, so reading `row.response` found nothing and every answer came
|
|
18
|
+
back a failure with no error to explain it. And the cancel route is `<id>:cancel`; the slash form
|
|
19
|
+
answers 404 (`DELETE` and `PATCH` on the bare batch answer 405). All three measured live against
|
|
20
|
+
`api.x.ai` on 2026-09-04, which is also when the corpus cell for xAI batch went from unsupported
|
|
21
|
+
to green in 17 seconds -- the fastest of the four providers.
|
|
22
|
+
|
|
23
|
+
- **A hosted tool the provider cannot run was dropped without a word.** Asking OpenRouter for
|
|
24
|
+
`code_interpreter` put `tools: []` on the wire and said nothing: the catalog is right (OpenRouter
|
|
25
|
+
routes function tools and its own plugins, not hosted code execution), so omitting it is correct,
|
|
26
|
+
but the silent loss of a capability the caller asked for is the exact thing this library's
|
|
27
|
+
tool-constraint mechanic exists to prevent. An unsupported builtin now produces a
|
|
28
|
+
`request_adjusted` warning naming the tool and what the provider does run, and an empty `tools`
|
|
29
|
+
array no longer reaches any provider -- it used to be dropped only on the `web_search` path.
|
|
30
|
+
|
|
31
|
+
- **The local chunker silently dropped space-free text: CJK prose, minified JSON, base64 blobs,
|
|
32
|
+
long URLs.** `snapStep` moved the cursor to the next ASCII space no matter how far away it was,
|
|
33
|
+
and to the END of the document when there was none -- in both cases without emitting a chunk for
|
|
34
|
+
the span it stepped over. So a document with a base64 image in the middle lost the image and
|
|
35
|
+
everything the walk skipped with it (measured: 17983 of 26991 characters), and one that turns
|
|
36
|
+
space-free and stays that way was indexed as its first window alone. The text was never embedded
|
|
37
|
+
and no query could retrieve it. A space is now a boundary only while the next window would still
|
|
38
|
+
start inside the chunk just emitted; past that the walk advances by the step. Verified live
|
|
39
|
+
against real OpenAI embeddings, with the answer inside a minified payload: no hit before, the
|
|
40
|
+
right hit after, for the payload at the end of the document and in the middle of it.
|
|
41
|
+
|
|
42
|
+
- **A document that mixed prose with a space-free run was chunked into dozens of runts.** The
|
|
43
|
+
step is derived from the SNAPPED window length, so a window trimmed back hard -- its only space
|
|
44
|
+
near its start, which is exactly what the last window before a base64 blob or a minified payload
|
|
45
|
+
looks like -- left a step below the overlap, and the `max(step, 1)` floor became the actual step.
|
|
46
|
+
The walk then crawled one word at a time across the whole approach to the blob: on a README with
|
|
47
|
+
one embedded image, 42 of 58 chunks came out under half the budget and the smallest was 8
|
|
48
|
+
characters. Every one was embedded at the caller's expense, and because they are the same
|
|
49
|
+
sentence shifted by a word they crowded each other out of the results. A boundary is now taken
|
|
50
|
+
only when it leaves at least half the window; below that the window is used whole. The cut that
|
|
51
|
+
buys can only ever land inside a run with no space in it for 1024 characters, where there is no
|
|
52
|
+
word to split. Same README: 16 chunks, no runts. Prose is byte-for-byte unaffected.
|
|
53
|
+
|
|
54
|
+
- **The tail of every locally-indexed document was embedded several times over.** Once a window
|
|
55
|
+
reached the end of the text the walk kept going, re-emitting the same tail as a run of
|
|
56
|
+
ever-shorter chunks that all ended in the same place. Not lost text -- duplicate index entries,
|
|
57
|
+
each one paid for at the embedding endpoint and each one competing with the others for a result
|
|
58
|
+
slot. The walk now stops on the window that reaches the end.
|
|
59
|
+
|
|
60
|
+
- **Every OpenAI model was routed to the Responses API, whatever the model.** `resolveApi` chose by
|
|
61
|
+
PROVIDER alone, while the catalog had carried `preferredApi` per model from the start and nothing
|
|
62
|
+
read it. Six catalogued models cannot be called on Responses at all: `gpt-audio`, `gpt-audio-1.5`,
|
|
63
|
+
`gpt-audio-mini` and the three `*-search` models, which are Chat Completions-only. Asking for any
|
|
64
|
+
of them failed with *"The requested model 'gpt-audio' is not supported with the Responses API"*.
|
|
65
|
+
Routing now consults the model's own preference and falls back to the provider default. Verified
|
|
66
|
+
live: `gpt-audio` returns audio and `gpt-5-search` answers, both of which previously could not run.
|
|
67
|
+
|
|
68
|
+
- **`gpt-audio*` was described wrongly by the catalog.** `preferredApi: 'responses'` (see above),
|
|
69
|
+
`outputModalities: ['text']` for a model that returns audio bytes AND a transcript, and
|
|
70
|
+
`capabilities.audioGeneration: false` for the audio-generation model. OpenAI's guide is explicit:
|
|
71
|
+
*"For this audio-chat pattern, use Chat Completions with an audio-capable model."*
|
|
72
|
+
|
|
73
|
+
- **Streamed audio produced nothing at all.** `delta.audio` was ignored by the Chat Completions
|
|
74
|
+
stream parser, so a `gpt-audio` turn streamed 68 SSE events and yielded exactly ONE unified event:
|
|
75
|
+
`usage`. No text (the words are in `audio.transcript`, not `delta.content`), no media, and —
|
|
76
|
+
because these chunks never carry a `finish_reason` — no terminal event either, so a caller
|
|
77
|
+
awaiting `done` waited forever. Now the transcript surfaces as `text`, the fragments as
|
|
78
|
+
`media_start` / `media_chunk` / `media_end`, and the final `expires_at`-only delta closes the
|
|
79
|
+
stream. Streamed audio is always `pcm16` (the API refuses any other format when `stream=true`) and
|
|
80
|
+
raw PCM has no magic bytes, so the mime is stated rather than sniffed.
|
|
81
|
+
|
|
82
|
+
- **Audio output was silently dropped on the OpenAI chat path.** `openai-completions` gated its
|
|
83
|
+
`modalities` block on `hasAudioInput`, so `outputModalities: ['text', 'audio']` travelled from
|
|
84
|
+
`ExecuteOptions` all the way to the wire builder and died there. `gpt-audio` then refused the call
|
|
85
|
+
outright — *"this model requires that either input content or output modality contain audio"*.
|
|
86
|
+
The parser had always known how to build an `audio_output` part from `message.audio`, so both ends
|
|
87
|
+
of the feature existed and only this guard kept them apart. The guard now fires on audio in OR
|
|
88
|
+
audio out. OpenRouter inherits the same spec, so models routed through it now forward the audio
|
|
89
|
+
request the caller actually made.
|
|
90
|
+
|
|
91
|
+
- **Every audio clip was labelled `audio/wav`, whatever it was.** OpenAI returns `message.audio` as
|
|
92
|
+
`{ id, data, expires_at, transcript }` — there is no `format` key — so the adapter's
|
|
93
|
+
`audio/${format ?? 'wav'}` fell through to the default on every response. Request mp3, receive
|
|
94
|
+
`ID3`-prefixed mp3 bytes, be told it is wav. A new `sniffAudioMime` reads the container from the
|
|
95
|
+
magic bytes (mp3/wav/ogg/flac/aac), mirroring the existing `sniffImageMime`; the old template
|
|
96
|
+
remains as the last resort.
|
|
97
|
+
|
|
98
|
+
### Changed
|
|
99
|
+
|
|
100
|
+
- **Stream parsing is spec-driven too.** The other half of the same migration: all seven
|
|
101
|
+
`createStreamParser` implementations now run one driver over a declarative spec, and **682 more
|
|
102
|
+
lines of hand-written parsing are gone**. The driver is the buffered interpreter with two
|
|
103
|
+
differences — state is created once per STREAM rather than per call, and one reserved
|
|
104
|
+
accumulator is drained and returned after each SSE event.
|
|
105
|
+
|
|
106
|
+
Measured before starting: of 518 lines across the five parsers, 38 touched state. The rest was
|
|
107
|
+
dispatch, which is what a spec expresses. Behaviour is unchanged and checked the same way, against
|
|
108
|
+
29 recorded streams whose event sequences are frozen in the corpus, plus one live streaming call
|
|
109
|
+
per provider after the switch.
|
|
110
|
+
|
|
111
|
+
`parseStreamEvent` stays on the adapter interface as a stateless one-shot; `createStreamParser` is
|
|
112
|
+
the stateful one callers should use.
|
|
113
|
+
|
|
114
|
+
- **Response parsing is spec-driven.** Requests have been built from specs since 3.0.0; the parse
|
|
115
|
+
side was seven hand-written `parseResponse` implementations doing the same four things in four
|
|
116
|
+
spellings. All seven now run one interpreter over a declarative spec, and **599 lines of
|
|
117
|
+
hand-written parsing are gone**.
|
|
118
|
+
|
|
119
|
+
The evaluator is the request one, unchanged: `$`, `$map`, `$call`, `$table`, `$join`, `$when` and
|
|
120
|
+
`$default` never cared what the root object was. Only classification is new — `collect` walks a
|
|
121
|
+
discriminated array and emits into named accumulators, and naming several places ONE object in
|
|
122
|
+
each rather than copies, which is what the adapters did and what consumers depend on.
|
|
123
|
+
|
|
124
|
+
Behaviour is unchanged, and that is checked rather than asserted: the differential replays every
|
|
125
|
+
recorded provider body through the adapters and compares against `parsed` values frozen in the
|
|
126
|
+
corpus — data neither path recomputes, so it still means something now the code that produced
|
|
127
|
+
it is deleted. 46 of 46 buffered cells match, and one live call per provider was made against the
|
|
128
|
+
real API after the switch.
|
|
129
|
+
|
|
130
|
+
`OpenRouterAdapter.parseResponse` is gone entirely: its `:online` web-search rule is a delta of
|
|
131
|
+
the shared spec, so naming the spec is the override now, exactly as `wireFlavor` already was for
|
|
132
|
+
requests.
|
|
133
|
+
|
|
134
|
+
### Internal
|
|
135
|
+
|
|
136
|
+
- **The response corpus covers the parse paths it never reached.** Measured against the fixtures,
|
|
137
|
+
`citations`, `files`, `builtinToolCalls`, `media` and `moderation` appeared in **zero** buffered
|
|
138
|
+
cells, and `thinking` in one target — so a parser that stopped producing any of them kept the
|
|
139
|
+
differential green. Five scenarios were added (`builtin.search`, `builtin.codeexec`, `media.audio`,
|
|
140
|
+
`thinking`, `moderation`), taking the corpus from 42 to 57 recorded cells. Both bugs above were
|
|
141
|
+
found by recording them.
|
|
142
|
+
|
|
143
|
+
Scenarios now declare which targets they apply to, because the matrix is no longer a full cross
|
|
144
|
+
product: Chat Completions has no hosted web search, and only `gpt-audio` returns audio. The
|
|
145
|
+
recorder and the differential read the same `expectedCells()`, so they cannot disagree about what
|
|
146
|
+
is missing.
|
|
147
|
+
|
|
148
|
+
- **The streaming corpus covers the branches that carry state.** Measured before this change, the
|
|
149
|
+
14 streaming cells produced only `usage`, `done`, `text`, `tool_call_*` and `thinking`: NINE of the
|
|
150
|
+
sixteen `StreamEvent` types had no coverage, and they were exactly the stateful ones — the
|
|
151
|
+
accumulate-JSON-then-pair-with-its-result machine, the emit-once-per-stream flags, the three-event
|
|
152
|
+
media reassembly. Five streaming scenarios were added (search, code execution, audio, thinking,
|
|
153
|
+
moderation), taking the corpus from 60 to 75 cells and streaming coverage from 7/16 to **15/16**.
|
|
154
|
+
The sixteenth, `error`, is emitted only by the Realtime adapter, which this corpus does not cover.
|
|
155
|
+
|
|
156
|
+
The audio bug above was found by the first run of the new cell: it tripped the existing invariant
|
|
157
|
+
that every streaming cell must produce a terminal event.
|
|
158
|
+
|
|
159
|
+
- **`XAIAdapter` emitted every reasoning delta TWICE.** Its `parseStreamEvent` override prepended a
|
|
160
|
+
`thinking` event for `reasoning_content` while `OpenAIAdapter`, its parent, already appended one
|
|
161
|
+
for the same field. `xai/completions` is not a corpus target — xAI defaults to the Responses
|
|
162
|
+
API — so nothing was watching. The override was both redundant and duplicating; it is gone, and
|
|
163
|
+
a regression test pins the count at one.
|
|
164
|
+
|
|
165
|
+
- **A test that failed on timing alone.** `tiktoken-optional` builds a counter per case, and the
|
|
166
|
+
encoder is a ~5.6 MB WASM module loaded lazily per instance, landing either side of bun's 5s
|
|
167
|
+
default under load. It failed twice in one session, on a different case each time, with no
|
|
168
|
+
assertion involved. The limit is raised rather than the load hidden: the work is slow, not wrong.
|
|
169
|
+
(A first attempt to warm the encoder in `beforeAll` made it fail 3/3 instead of intermittently,
|
|
170
|
+
because the cache is per-instance and the warm-up only added a third load.)
|
|
171
|
+
|
|
172
|
+
- **The corpus did not catch everything, and that is worth recording.** Switching the adapters
|
|
173
|
+
dropped xAI's inline code-execution file extraction: `XAIResponsesAdapter` overrides
|
|
174
|
+
`filesFromOutputItem`, the shared spec transform called the OpenAI module function directly, and
|
|
175
|
+
the override was simply never consulted. No recorded xAI cell runs code execution, so the response
|
|
176
|
+
differential stayed green — an existing unit test failed instead. xAI now has its own response
|
|
177
|
+
registry, the parse-side twin of that adapter override.
|
|
178
|
+
|
|
179
|
+
- **The three parse branches for a failure reported inside a 200 are covered.** A provider cannot be
|
|
180
|
+
asked to fail on demand, so those cells are CONSTRUCTED — marked `synthetic: true`, built from the
|
|
181
|
+
target's own recorded envelope with only the failure fields changed, each traceable to the official
|
|
182
|
+
SDK type cited in the cell's `provenance`. They cover OpenAI Responses `status:'failed'` with
|
|
183
|
+
`response.error`, its `incomplete_details.reason: 'content_filter'` (which must not be reported as a
|
|
184
|
+
length truncation), and Google Interactions `status:'failed'`.
|
|
185
|
+
|
|
186
|
+
A synthetic body widens the derived shape book's `known` and `values` but is excluded from
|
|
187
|
+
`expected`: a failed response carries no `output`, and letting it into that intersection would
|
|
188
|
+
weaken the check on every genuine recording.
|
|
189
|
+
|
|
190
|
+
- **The frozen request corpus grew a waiver list.** A deliberate wire change previously had no way to
|
|
191
|
+
be recorded except re-freezing, which would absorb every *un*noticed change in the same pass. Each
|
|
192
|
+
waiver is checked in both directions: one whose cells no longer differ fails the suite, so it
|
|
193
|
+
cannot outlive the change it describes.
|
|
194
|
+
|
|
195
|
+
## [3.2.1] — 2026-08-31
|
|
196
|
+
|
|
197
|
+
### Fixed
|
|
198
|
+
|
|
199
|
+
- **Three TypeScript errors in an MCP test file** that made v3.2.0 unpublishable: `searchParams.get()`
|
|
200
|
+
returns `string | null` while `.at(-1)` returns `string | undefined`, and `toBe` has no overload
|
|
201
|
+
spanning both. The assertions are unchanged; only their types are aligned.
|
|
202
|
+
|
|
203
|
+
- **Two lint warnings** left standing in `network/engine.ts` (an unused type import) and an MCP test
|
|
204
|
+
helper (`let x!` forward declarations that nothing forward-references).
|
|
205
|
+
|
|
206
|
+
### Internal
|
|
207
|
+
|
|
208
|
+
- **The release gate now runs lint, typecheck and the tests** (`G1 build-green` in CombyCode's shared
|
|
209
|
+
quality-gate). v3.2.0 was tagged with a green gate and a red typecheck, because the gate checked
|
|
210
|
+
documentation and consumers while lint and typecheck lived in a playbook sentence — the half a
|
|
211
|
+
human has to remember. The gate's own README names that failure mode: "a checklist you have to
|
|
212
|
+
remember to read is guarded by the same attention that failed in the first place." The check ships
|
|
213
|
+
with the good/bad/blind fixtures the selftest requires, and was verified against the exact error
|
|
214
|
+
that escaped.
|
|
215
|
+
|
|
216
|
+
## [3.2.0] — 2026-08-31
|
|
217
|
+
|
|
218
|
+
### Added
|
|
219
|
+
|
|
220
|
+
- **`response.citations` — the sources an answer cited, unified across providers.** Reaching them
|
|
221
|
+
meant regexing `response.raw`; the SDK's own web-search example did exactly that, which is every
|
|
222
|
+
consumer reimplementing provider knowledge that belongs here. Four wire shapes are read: Anthropic's
|
|
223
|
+
text-block `citations[]` (the only provider that also reports the cited passage), Google's
|
|
224
|
+
`groundingMetadata.groundingChunks[]`, OpenAI Responses/Chat `url_citation` annotations, and xAI's
|
|
225
|
+
bare top-level `citations[]`.
|
|
226
|
+
|
|
227
|
+
Distinct from `builtinToolCalls`, which records what the model *invoked*: a turn can run three
|
|
228
|
+
searches and cite one page. Through an agent run the sources **accumulate across steps**, deduped
|
|
229
|
+
by URL — a run that searches in step 1 and answers in step 3 keeps the sources its answer rests on.
|
|
230
|
+
Optional per R3 (`response.citations ?? []`); absent on `stream()`, which holds no raw payload, and
|
|
231
|
+
on Google's Interactions surface, whose grounding shape has not been measured.
|
|
232
|
+
|
|
233
|
+
**Streaming reports them too**, as a `citation` StreamEvent per source, collected onto the streamed
|
|
234
|
+
final response so `stream()` and `complete()` agree. Four more measured shapes, since a stream never
|
|
235
|
+
assembles the body the buffered reader parses: Anthropic `citations_delta`, Responses
|
|
236
|
+
`response.output_text.annotation.added` (OpenAI and xAI), chat-completions `delta.annotations`, and
|
|
237
|
+
Google's *late* populated `groundingMetadata` chunk — the first one carrying that key is empty, so
|
|
238
|
+
latching on first sight would report a search and no sources.
|
|
239
|
+
|
|
240
|
+
Verified live on Anthropic, OpenAI, Google, xAI and OpenRouter, buffered and streamed: 5/5 providers
|
|
241
|
+
return real cited URLs for the web-search scenario, which previously reported `no-citation` on all
|
|
242
|
+
five. Note Google reports each source as a `grounding-api-redirect` URL, not the page itself.
|
|
243
|
+
|
|
244
|
+
### Fixed
|
|
245
|
+
|
|
246
|
+
- **xAI file `delete`, `getInfo` and `list` never worked.** `XAIFileAdapter` is exported, and three
|
|
247
|
+
of its four methods rejected with `unknown spec: xai/files.<op>` before any HTTP — only
|
|
248
|
+
`xai/files.upload` had ever been written, so roughly twenty lines of response mapping below them
|
|
249
|
+
had never run. The three specs follow `xai.upload`'s own note that the surface differs from
|
|
250
|
+
OpenAI's only in the upload `purpose`, so they carry the same paths and the same Bearer base.
|
|
251
|
+
|
|
252
|
+
- **`listModelsLive` was blocked by CORS in the browser, for Anthropic only.** Anthropic refuses a
|
|
253
|
+
browser request without an explicit opt-in header. The chat path sends it and the Anthropic files
|
|
254
|
+
specs carry it, but `models.list` lost it when that request moved to a spec: the header had lived
|
|
255
|
+
in the helper's own table, which nothing called any more. Measured from a browser with a raw
|
|
256
|
+
fetch — `/v1/models` returns 200 with the header and fails without it. The sandbox listed models
|
|
257
|
+
through `Promise.allSettled` and kept only the fulfilled results, so the rejection was swallowed
|
|
258
|
+
and Anthropic simply showed no models rather than an error.
|
|
259
|
+
|
|
260
|
+
### Removed
|
|
261
|
+
|
|
262
|
+
- **Five private functions left behind by the spec migration.** Each was live until its area moved to
|
|
263
|
+
the spec-driven builder, which renamed it with a leading underscore instead of deleting it:
|
|
264
|
+
`_buildForm`, `_batchName`, `_toOpenAIAudioFormat`, `_toResponseModalities` and
|
|
265
|
+
`LiveSpec.headers`. Each was checked field by field against the spec that replaced it before
|
|
266
|
+
removal — and the last of them was **not** equivalent, which is how the CORS bug above was found.
|
|
267
|
+
|
|
268
|
+
### Internal
|
|
269
|
+
|
|
270
|
+
- **Line coverage 91.62% → 99.53%**, function coverage 87.53% → 97.41%, across 2290 → 4051 tests.
|
|
271
|
+
Eleven modules had no function coverage at all — `batcher.ts` ran 3.5% of its lines,
|
|
272
|
+
`plugins/internal-tools/runner/runner.ts` 3.7%, `transport-ws.ts` 9.6%. Every new test is mutation-verified: the
|
|
273
|
+
source line it claims to cover was broken and the test watched to fail.
|
|
274
|
+
|
|
275
|
+
The exercise found ten defects no existing test caught, each pinned as a `DEFECT:` test rather
|
|
276
|
+
than fixed in the same pass: `ResponseStore.list(null)` returning every user's response ids,
|
|
277
|
+
`chunker.ts` silently dropping space-free text (3000 characters of CJK or base64 yield one
|
|
278
|
+
400-character chunk), `attachment.ts fromBlob` leaving the MIME type empty because a type-less
|
|
279
|
+
`Blob` reports `''` rather than `null`, and `oauth.ts tryRefresh` discarding the refresh token it
|
|
280
|
+
means to preserve.
|
|
281
|
+
|
|
282
|
+
Two pre-existing tests turned out to execute no code at all — one asserts against `readFileSync`
|
|
283
|
+
of `realtime.ts` with regexes over the source text, which is why that file sat at 0% functions
|
|
284
|
+
while appearing tested.
|
|
285
|
+
|
|
286
|
+
- **A coverage floor that can actually fail.** `bunfig.toml`'s `coverageThreshold` is accepted and
|
|
287
|
+
ignored by bun 1.3.14 — set to an impossible 1.0 the run still exits 0 — so the floor is its own
|
|
288
|
+
step (`bun run coverage:gate`), carrying a `--self-test` that proves it discriminates.
|
|
289
|
+
|
|
290
|
+
|
|
9
291
|
## [3.1.0] — 2026-08-25
|
|
10
292
|
|
|
11
293
|
### Added
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Mutable accumulator for a single agent loop step.
|
|
2
2
|
* Passed through stream-event helpers so they don't fight closure state. */
|
|
3
3
|
import type { ToolCallPart } from '../llm/types/messages';
|
|
4
|
-
import type { Usage } from '../llm/types/response';
|
|
4
|
+
import type { Citation, Usage } from '../llm/types/response';
|
|
5
5
|
/** Accumulation bucket for one in-progress tool call (before tool_call_end). */
|
|
6
6
|
export interface ToolCallAccumEntry {
|
|
7
7
|
id: string;
|
|
@@ -19,4 +19,8 @@ export interface StepState {
|
|
|
19
19
|
toolCallAccum: Map<string, ToolCallAccumEntry>;
|
|
20
20
|
stepUsage: Usage;
|
|
21
21
|
stepFinishReason: string;
|
|
22
|
+
/** Sources cited during this step, keyed by url. A Map rather than an array
|
|
23
|
+
* because Google repeats its grounding chunks across late chunks, and one page
|
|
24
|
+
* cited twice is one source. */
|
|
25
|
+
stepCitations: Map<string, Citation>;
|
|
22
26
|
}
|
|
@@ -66,6 +66,18 @@ export interface ModelReasoning {
|
|
|
66
66
|
effortValues?: string[];
|
|
67
67
|
encryptedContent: boolean;
|
|
68
68
|
summaryAvailable: boolean;
|
|
69
|
+
/** Whether `thinking: { mode: 'off' }` can actually be honoured.
|
|
70
|
+
*
|
|
71
|
+
* Deliberately optional, and deliberately not defaulted. Absent means NOBODY
|
|
72
|
+
* HAS ESTABLISHED IT for this model, which is not the same as `true`: only an
|
|
73
|
+
* explicit `false` makes the client drop the request and warn, so a provider
|
|
74
|
+
* nobody has measured keeps behaving exactly as it did.
|
|
75
|
+
*
|
|
76
|
+
* It exists because some models refuse: Gemini 2.5 Pro answers `Budget 0 is
|
|
77
|
+
* invalid. This model only works in thinking mode.`, and 3.7 Flash accepts a
|
|
78
|
+
* zero budget and reasons anyway. Sending the disable field to those buys a
|
|
79
|
+
* 400 or a silent lie; saying so up front costs a warning. */
|
|
80
|
+
canDisable?: boolean;
|
|
69
81
|
}
|
|
70
82
|
export interface TokenizerInfo {
|
|
71
83
|
strategy: 'heuristic' | 'tiktoken' | 'count_api';
|