# Changelog

## 3.0.1

### Patch Changes

- 3641102: Prompt caching now places explicit breakpoints instead of sending a top-level
  `cache_control: { type: 'auto' }`, a value OpenRouter does not define (its
  top-level field takes `{ type: 'ephemeral' }` and puts the breakpoint on the
  last cacheable block — with request-only context at the tail, the part the next
  request replaces).

  For `anthropic/`, `google/gemini` and `qwen/` models, the models OpenRouter
  documents as needing explicit breakpoints, a cache request marks the last
  static system message and the last non-system message before request-only
  context, each inside a content part (a string becomes one text part). A system
  message directly before the context is the step's guidance, which changes from
  step to step, so it is passed over rather than marked. Other models, which
  OpenRouter documents as caching automatically, are sent no marker and keep
  their plain string content.

## 3.0.0

### Major Changes

- e83dfe5: `listModels()` omits `inputPrice` and `outputPrice` for a model this driver has no rate for, instead of reporting the rate as `0`.

  The returned `ModelInfo[]` is the published signature that moved: both fields are optional on it now, so read them as `number | undefined`. Their runtime values changed too, and that half is not caught by a type — a key that used to be present is now absent, and code that defaulted it with `?? 0` will keep doing exactly what this release stopped doing for you.

  What each driver does now:

  - **`@namzu/anthropic`** — the live listing from `models.list` carries no rates, so it omits both. The bundled offline catalogue still carries its real published prices, unchanged.
  - **`@namzu/openai`** — `client.models.list` publishes no rates, so both are omitted. `codex` likewise.
  - **`@namzu/deepseek`** — the account listing and the bundled known-model list are unrated here, so both are omitted.
  - **`@namzu/google`** — the two-row price table still prices `gemini-2.5-flash` and `gemini-2.5-pro`; every other model the API returns is now unpriced rather than free.
  - **`@namzu/openrouter`** — a model whose listing carries no `pricing` block is unpriced. A model the vendor prices at `"0"` is still free and still reports `0`, which is the distinction this release is about.

  An absent rate means unknown, and a consumer that shows one should say so rather than `$0.00`. `@namzu/sdk`'s `ModelInfo.inputPrice` carries the reasoning.

### Minor Changes

- e5411cc: Send and surface reasoning.

  The driver now carries the SDK's thinking controls onto OpenRouter's unified `reasoning`
  object — `thinking: { type: 'enabled', budgetTokens }` becomes `{ enabled: true, max_tokens }`,
  `adaptive` becomes `{ enabled: true }`, `disabled` becomes `{ enabled: false }`, and `effort`
  becomes `{ effort }` — and streams the model's thinking back on the reasoning channel
  instead of dropping it. `effort: 'max'` and `effort: 'ultra'` are refused rather than sent:
  this wire cannot carry those levels, and an effort that arrives as a different depth is
  indistinguishable from one that was honoured.

  What a host observes: a caller that set `thinking` or `effort` used to get
  `OpenRouterProvider does not implement thinking`; the request is now made. Reasoning blocks
  arrive on the assistant message for every model that returns them (verified against
  `nvidia/nemotron-3.5-lightning:free`, which returns both `reasoning` and `reasoning_details`),
  and `usage.reasoningTokens` reports the thinking share of the completion tokens the vendor
  already billed. The text is emitted once, not twice: OpenRouter sends it in both fields, and
  the indexed `reasoning_details` form is the one used.

## 2.4.1

### Patch Changes

- 8d5223b: Nothing a consumer installs or calls changes, and that is the whole of this
  release. `vitest` moves from `^3.2.6` to `^4.1.11` in the `devDependencies` of
  all nineteen packages that declared it, and `@vitest/coverage-v8` moves with it
  in `@namzu/sdk`. Every occurrence is a devDependency — checked, not assumed —
  so `dependencies`, `peerDependencies`, exports, types, defaults and the wire
  shape are untouched, and the published tarballs differ from the previous
  release only in `package.json#devDependencies`.

  The reason is a security fix with no 3.x backport. `GHSA-82fw-gwwq-j7x9`
  ("Path Traversal / Arbitrary File Read via `@vitest/mocker` Redirect Mock")
  covers `vitest` and `@vitest/mocker` from `2.1.0` up to `4.1.11`, so `^3.2.6`
  can only be resolved by leaving the 3.x line. `4.1.11` is the first patched
  release and is what the lockfile now resolves for both.

  What this costs anyone who works on the repository rather than with it: the
  upgrade was not a version bump. Vitest 4 changed test discovery, coverage
  configuration, mock construction and reporter output, and each of those broke
  something here that had to be migrated rather than worked around. Those fixes
  are all under `__tests__/`, `vitest.config.ts` files and `scripts/`, none of
  which is published, which is why this is a patch and not a major.

  You do not need to do anything. If you pin `vitest` yourself to run this
  project's own suites, note that the config files it ships are now written for
  `>= 4.1.11` and will not run under 3.x.

## 2.4.0

### Minor Changes

- c635b5a: Use `namzu --output-schema /absolute/path/schema.json` for native schema-constrained TUI answers. Unsupported or lossy schema conversion fails at launch; normal conversations remain unchanged. Supply the flag again on resume.

  Enable native query admission for Codex, OpenRouter, DeepSeek, HTTP and Zen wire mappings. Codex forwards Responses text.format; HTTP maps schemas for both dialects. Zen messages requests use native format instead of hidden tool fallback, and Google requests preserve JSON Schema constraints. Endpoint/model support is still required and vendor errors remain errors.

  Breaking for direct HTTP/Zen callers: an Anthropic-dialect response format can no longer be silently ignored or fall back to an output tool. Schema-free JSON and explicit strict:false are rejected. Use strict native JSON Schema on a capable model, or choose SDK structuredOutput.mode="tool" when native schema output is unavailable.

  Anthropic transport retries now default to zero instead of the vendor SDK default of two. The host immediately receives classified HTTP 429 responses with Retry-After metadata instead of waiting invisibly inside the vendor client. Set AnthropicConfig.maxRetries to 2 to retain the former transport retry behavior.

  Rate-limit guidance no longer claims automatic retries were exhausted when retry policy may have disabled them or refused the requested delay.

## 2.3.2

### Patch Changes

- 36248f3: Add separate provider capability declarations for image and document tool
  results, and warn immediately before a request would degrade newly produced
  rich tool output. Tool presenters can now mark a generic label as a complete
  activity and mark a redundant successful acknowledgement as hidden; older
  hosts continue to render the same generic label.

  The account-routed Responses transport now sends supported user images and
  image tool results as ordered image input parts. Documents, unresolved stored
  references, unsupported image media types and unprojected omission markers are
  refused before transport.

  The interactive transcript now follows the visible conversation tail without
  a synthetic viewport-height gap, responds to terminal resize, narrates desktop
  actions with human labels, hides only successful empty acknowledgements, and
  keeps screenshot dimensions and failures visible.

## 2.3.1

### Patch Changes

- 15f8ee4: Bound provider stream silence, including query-owned advisory calls and
  RouterAgent routing decisions, compaction verifiers and model-graded eval
  judges, to five minutes by default and abort the stalled provider transport,
  with network-classified retry and fallback recovery where those policies
  apply. This changes the previous default, under which a provider iterator could
  remain silent forever. Set `streamIdleTimeoutMs: 0` on the run, agent, manual
  compaction, verifier, or judge config to keep the old unbounded behavior, or set
  a positive millisecond value to choose a different bound.

  Queries whose caller signal is already aborted now settle as cancelled before
  starting provider, provider-metadata, or tool work. A later cancellation also
  settles while an optional context-window resolver remains pending, even when
  that resolver ignores its signal. With no caller cancellation, `timeoutMs`
  bounds the optional metadata lookup, aborts its private transport signal, and
  falls back to the static context-window table instead of blocking the run.

  The OpenRouter context-window lookup now forwards cancellation to its model-list
  transport. Only fulfilled listings are cached, so cancelling one concurrent
  query cannot abort another query's shared metadata request or force that query
  onto the static context-window table.

  `runExperiment({ timeoutMs })` now applies one validated wall-clock deadline to
  both case execution and scoring. Scorers receive its optional cancellation
  signal; a non-cooperative scorer is detached, and `judgeScorer` forwards the
  signal to its bounded provider transport. Values outside the positive platform
  timer range are refused before a case starts; omit the field for the prior
  unbounded case behavior.

  Compaction verification inside a query now carries the run cancellation cause
  to its provider transport without placing a second idle timer around retry and
  fallback. Public `buildVerifiedSummary`, `compactNow`, and `compactRegion`
  calls bound raw provider silence themselves and accept optional `signal` and
  `streamIdleTimeoutMs`; malformed values and pre-cancelled manual work are
  refused before provider work or a no-op result.

  HTTP embedding batches now have a 30-second whole-request default, including
  response-body reads, where the previous default could wait forever. Set
  `requestTimeoutMs: 0` on `HttpEmbeddingProvider` to keep the former unbounded
  behavior. Invalid timeout values and non-positive or fractional `batchSize`
  or `dimensions` values are refused at construction instead of silently
  disabling the bound or entering a non-progressing batch loop. Successful HTTP
  responses must contain exactly one unique, in-range result per input and finite
  vectors of the configured dimension; malformed or incomplete batches are
  refused atomically instead of reaching ingestion with missing embeddings.

  Public RAG operations accept optional cancellation context. The shipped
  `knowledge_search` tool forwards its run-owned signal through
  `KnowledgeBase`, retrieval or ingestion, and the embedding provider. The HTTP
  provider preserves the caller's exact cancellation reason while aborting only
  its private fetch transport. Custom embedding providers receive the signal as
  a cooperative request; callers still own their wait boundary if a custom
  implementation ignores it. Default retrieval and ingestion recheck authority
  after that custom call settles, so a late result cannot start a vector search
  or persist chunks after cancellation. `VectorStore.search` and `upsert` now
  receive the same optional operation context. The default pipelines also race
  those store promises against cancellation, so a non-cooperative custom store
  cannot leave the public query or ingestion call pending forever.

  A2A agent-card discovery now has a 30-second whole fetch-and-body default and
  accepts an optional caller signal and `timeoutMs`; set `timeoutMs: 0` to retain
  the former unbounded behavior. `A2ADelegate.timeoutMs` now starts before
  `message/send` and bounds the whole delegation instead of polling only. A
  pre-cancelled dispatch starts no remote work, pending fetch and body promises
  cannot hold `waitForTask`, and caller cancellation preserves its exact cause on
  the private transport. Poll and delegation timers are validated at
  construction. Once a safe task id exists, cancellation or timeout sends one
  independently bounded `tasks/cancel`; during initial task creation the client
  keeps a short cleanup grace and explicitly reports an unknown remote outcome if
  the peer never returns an addressable id. Poll replies are bound to that initial
  id, and transport or protocol failures after it is known make the same bounded
  cleanup attempt before the original failure is returned. An `input-required`
  task is also bounded-cancelled before the delegate reports that it cannot
  supply the requested input.

  Connector execution now carries optional operation authority through the
  manager, every connector-tool adapter, real query runs, tenant/environment
  facades, health checks, and `MCPConnectorBridge.callTool`. Custom connectors
  receive the signal; if they ignore it, the manager settles with an honest
  unknown remote outcome and rejects a late success that does not identify a
  received response. A tenant call cancelled before admission no longer spends a
  rate-limit slot.

  `HttpConnector` and `WebhookConnector` now apply one validated 30-second
  fetch-and-body deadline and a streaming 2 MiB response limit by default. Set
  positive `timeoutMs` and `maxResponseBytes` values to choose different bounds.
  Cancellation, deadline, or response-size failure aborts only the private
  transport/body reader and preserves the caller's exact cause. Result metadata
  distinguishes `not_started`, `unknown`, and `response_received`, includes retry
  safety, and keeps a received status visible when its body is unavailable.

  Dynamic HTTP paths and webhook URL overrides must remain on the configured
  origin. Model-authored routing headers are refused, redirects are not followed,
  and 3xx responses are no longer reported as success. Configure a separate
  connector instance for each authorized origin; callers that previously used a
  cross-origin webhook override must migrate to that instance.

  `GuardedFetchProvider` now applies one validated 30-second deadline across DNS
  resolution, every manually admitted redirect fetch, and the final response
  body, while preserving a caller's exact cancellation cause on a private
  transport signal. Its 2 MiB default response cap is enforced from streamed
  bytes rather than after `response.text()` allocates the whole body; overflow
  cancels the reader and returns a valid UTF-8 prefix. Redirect bodies are
  cancelled when abandoned, and a spent redirect budget causes no DNS lookup for
  the next target. Set positive `timeoutMs` and `maxBytes` values or a
  non-negative integer `maxRedirects` to choose other bounds. Custom
  `GuardedFetchConfig.resolve` functions may now accept the operation signal as
  a second argument. IPv4-mapped IPv6 literals are canonicalized back to their
  IPv4 address before range checks, closing the hexadecimal mapped loopback and
  link-local bypass; the full IPv6 link-local and multicast ranges are also
  refused.

  MCP request methods now accept optional cancellation authority, and generated
  MCP tool and prompt adapters forward the run-owned tool signal. A pre-aborted
  request starts no transport work; a pending request preserves the caller's
  exact cause, aborts a private transport, removes its correlated pending id, and
  makes a one-second best-effort `notifications/cancelled` attempt. The
  notification does not prove that an already-started remote side effect stopped.
  Paged list calls recheck the same signal before each page.

  `MCPClient.requestTimeoutMs` and HTTP MCP transport `timeoutMs` values must now
  be positive platform-range integers. A shorter transport deadline remains a
  request-timeout terminal and emits the same correlated cancellation. HTTP
  fetches and response-body reads share operation authority; disconnect owns
  active requests and cancellation cleanup. Reconnects fence late POST responses
  and SSE batches from prior generations, clear Streamable session state, and
  accept session ids only from successful `initialize` responses. Per-send
  failure no longer marks a Streamable client connection-wide errored or rejects
  unrelated concurrent calls. `MCPTransport.send` now accepts optional
  `MCPTransportSendOptions`; custom transports should refuse pre-aborted work and
  stop their per-send I/O when its signal fires.

  Provider model listings and credential probes now accept optional cancellation
  signals. Retry, fallback, stream-idle and instrumentation decorators preserve
  that authority, and every bundled CLI driver forwards it to the underlying
  transport where supported or refuses a result that arrived after cancellation.
  Existing zero-argument provider implementations remain valid.

  The interactive provider picker now cancels model discovery, credential checks
  and subscription sign-in when the operator backs out, supersedes the work, or
  leaves the screen. Late results cannot reopen an old model step, accept a
  credential, re-probe the application, or persist a subscription credential
  after cancellation. Model listing and credential probing both settle after a
  three-second bound even when a custom provider ignores its signal.

  Between-turn and durable-resume subscription refreshes now settle on caller
  cancellation and apply one 30-second bound across the token request and response
  body. Refreshes in one session are serialized and re-read their source at the
  head of the queue, preventing a later stale caller from downgrading a token
  published by an earlier one. Namzu's credential file uses an exact conditional
  replacement under a cross-process, atomically published lock; an external
  rotation or logout wins, and an uncertain publication refuses instead of using
  an uncommitted refresh. Borrowed macOS Keychain credentials are read-only: a
  changed or removed entry wins, and a successful refresh of an unchanged entry
  remains session-local.

## 2.3.0

### Minor Changes

- 03e363c: Declare the Node floor these packages already had, and export a type `TelemetryConfig` already required.

  **`engines.node: ">=20.0.0"`.** Only `@namzu/cli` declared one; the other fourteen published without any, so npm could not warn a consumer installing onto an unsupported runtime — they got a crash at some later import instead. The floor is not new: `@namzu/cli` has declared it since it shipped and `install.sh` has enforced it since it existed. This makes the other fourteen say the same thing.

  If you install with `engine-strict=true` on Node 18, an install that previously emitted nothing will now fail. Upgrade to Node 20 or newer, which the code already assumed. Everyone else sees no change, or an `EBADENGINE` warning that replaces a later crash.

  Worth stating plainly: CI verifies Node 22 and 24. The 20 floor is a declared minimum, not a tested one.

  **`SpanProcessorLike` is now exported from `@namzu/telemetry`.** `TelemetryConfig.spanProcessors` takes `readonly SpanProcessorLike[]`, and the type had no export — a field on the public surface whose type was not on it, so a host supplying the value had to inline the shape or reach for `any`.

## 2.2.1

### Patch Changes

- 5394981: Make each driver's README an npm package page rather than its manual.

  Every driver README carried its full reference — configuration tables, capability matrices, error surfaces — between 167 and 392 lines of it. That is a reasonable shape for a single-package repository, where the README _is_ the documentation, and the wrong one for a package in a monorepo that has a `docs/` tree: it duplicates what the docs say, and nothing checks that the two agree.

  The README is now what a reader needs in the first minute — what the driver is, install, one working example, links. The reference moved to `docs/providers/<name>.md`, whole, and its code samples are now compiled against the built SDK by the doc-fence gate on every CI run. They never were before; several did not compile.

  No API change.

## 2.2.0

### Minor Changes

- 70f23bb: A driver can now say how large a model's context window is, and the kernel
  ranks that above its hand-maintained table.

  That table was the only source below an explicit host config, and its own
  header records what it cost: every Claude entry carried 200k including the
  1M-window models, so those runs compacted at roughly 14% full and threw
  away the prompt-cache prefix to do it. Every model release drifts it again
  until somebody edits it — while the OpenRouter driver was already parsing
  the vendor's real `context_length` and discarding it, because there was no
  member to return it through.

  `LLMProvider.resolveContextWindow?(model, signal)` is three-state like
  `effortLevelsFor`: absent means this driver cannot answer, a resolved
  `undefined` means it asked and does not know, a number is the answer. A
  driver resolving `undefined` falls through to the TABLE, not to the
  assumed default — asking must never be worse than not asking.

  Resolved once per run, at the door. Both consumers are synchronous and in
  the hot loop, so this can never become an await inside it. A driver that
  rejects or hangs does not fail the run: the window is an optimisation over
  a working default.

  `ResolvedContextWindow['source']` and the `windowSource` on
  `token_usage_updated` gain `'provider'`, ranked between `'config'` and
  `'model-table'`, so a host can see which route a number came from.

  Also fixes a hole this exposed: `withProviderRetry` and
  `withProviderFallback` forwarded `listModels`, `healthCheck` and
  `doctorCheck` but not `effortLevelsFor`. A dropped optional member does not
  fail — it reads as "this driver cannot answer" — and retry is on by
  default, so a driver's declared effort levels were invisible on
  essentially every run.

## 2.1.0

### Minor Changes

- d3bd080: A wrong API key is no longer reported as working

  Typing a key into the picker ran a check that could not fail for two providers.
  Measured against deliberately invalid keys, both said the key was good.

  **With an OpenRouter key, any string at all passed.** A typo, the wrong
  clipboard entry, a revoked key — all were accepted and reported as verified. The
  check listed the model catalogue and treated a successful list as a passed
  check, and OpenRouter's catalogue endpoint does not authenticate, so it answered
  the same way whatever was sent. Nothing was wrong with that driver's listing; a
  catalogue was simply never evidence about a key.

  **With an Anthropic key, a real rejection was discarded.** The listing caught
  the `401` and returned a hardcoded three-model list, which the check read as
  success — so the truth existed, was thrown away, and was replaced by something
  that looked like an answer.

  A credential check is now a separate, declared capability. A driver that
  declares no probe is reported as **not checked**, never as verified, so a driver
  added in future cannot silently inherit a check it does not perform. Anthropic,
  OpenRouter, OpenAI and Ollama declare one; OpenRouter's asks about the key
  rather than the catalogue.

  Refusal and doubt stay distinct. A `401` means the key is genuinely refused; a
  timeout or a DNS failure means nothing was learned, and is reported that way —
  telling someone on a broken connection to rotate a working key is a different
  error, not a smaller one.

  **Anthropic's model listing also never once ran.** The SDK method was pulled out
  of its namespace and called bare, so it lost `this`, threw a `TypeError` on
  every call, and was swallowed by the same catch — the hardcoded models were not
  a fallback but the only answer the method could give. It now calls the live
  endpoint, and falls back only when that genuinely fails.

  The four driver packages are `minor` rather than `patch`: each gains a method
  it did not have, and added functionality is a minor whatever the size of the
  diff. Anthropic's earns it twice over, because its listing now returns the live
  catalogue where it previously returned the same three hardcoded entries to every
  caller - so the value every existing caller receives changes.

## 2.0.1

### Patch Changes

- 48d9d67: Published tarballs no longer contain test files.

  `files: ["dist", "src", ...]` reads as "the build output and the sources" and
  means "everything the compiler emitted and everything in the tree", so every
  compiled test, its declaration, and both source maps shipped to the registry —
  and for the twelve packages that also ship `src`, the raw test sources went with
  them.

  Measured on the versions currently published:

  | package      | files       | of which tests | unpacked           |
  | ------------ | ----------- | -------------- | ------------------ |
  | `@namzu/sdk` | 3879 → 2239 | 1640 (42%)     | 12.73 MB → 6.81 MB |
  | `@namzu/cli` | 462 → 282   | 180 (39%)      | 1.21 MB → 0.73 MB  |

  Nothing you can import changes. Every package restricts `exports` to `"."`, so
  Node refused a deep subpath into those files already — they were weight in the
  tarball and nothing else. Hence `patch`: there is no consumer-visible surface
  here, only less to download.

  The exclusions are at the packaging layer, not the compiler. Adding `exclude`
  to `tsconfig.json` would have kept tests out of `dist` and also dropped them
  from `tsc --noEmit`, silently ending type-checking of the entire test suite —
  trading a packaging defect for a much worse one.

## 2.0.0

### Major Changes

- 1500973: Every driver that cannot think now says so instead of dropping the request.

  `thinking` sits on `ChatCompletionParams`, so every driver accepts it. Five of
  them — Bedrock, OpenRouter, HTTP, Ollama, LM Studio — implemented none of it
  and dropped the field: the caller got an ordinary completion with an empty
  `reasoning` array, which is indistinguishable from a model that simply chose
  not to reason. The request looked honoured and the answer looked like an
  answer.

  The OpenAI driver already refused instead, with the reasoning written out
  beside it. So the rule had been decided once and applied once, while five
  siblings went on being silent. It moves to `@namzu/sdk` as
  `assertThinkingUnsupported(driverName, params)`, and a new driver now inherits
  it rather than re-deciding it.

  The error names the driver, which in a multi-provider setup is the difference
  between a bug report about the model and a one-line configuration fix.

  **Turning thinking off stays a no-op** on all of them, because that is the
  state a driver without thinking is already in — a config shared across
  providers saying `{ type: 'disabled' }` should not fail on the ones that were
  never going to think.

  `assertThinkingSupported` in `@namzu/openai` is unchanged as an export and now
  delegates to the shared helper. Its message changed: it no longer says
  "extended thinking", because `adaptive` is refused too and calling that
  extended would be wrong.

  **Migration.** If you passed `thinking` to any of the five and relied on it
  being ignored, remove it — you were receiving a non-thinking answer either way,
  and now you find out at the call instead of by inspecting an empty array.

  Not in this change: implementing thinking natively on Bedrock, which serves the
  same Claude models through a different wire and deserves the per-model
  resolution the Anthropic driver just gained. That needs the Converse request
  and response shapes verified against the reference first, and is not something
  to guess at.

## 1.1.0

### Minor Changes

- 935b8f3: Every driver can now see.

  These three dropped `attachments` outright, so a user who attached a
  screenshot got a turn about nothing. `supportsVision: false` said so, which
  made the declaration honest and the driver useless — and it was the last
  place in the estate where a namzu capability existed on one driver and
  silently did not on another.

  Each wire carries an image differently, so this is one intent and three
  mappings:

  - **Converse**: raw bytes in an image content block beside the text. The
    tool-result path already did this; the user path never looked at
    `attachments`.
  - **The two-dialect HTTP driver**: a `data:` URI content part on one
    dialect, a base64 source block on the other.
  - **The gateway driver**: a `data:` URI content part.

  Across all three: a media type the endpoint cannot decode is named in the
  text rather than sent, because a payload it rejects fails the whole request
  and losing the turn is worse than losing sight of one image. A message with
  no attachments keeps its plain-string content, so nothing about an ordinary
  request changes shape. Several attachments on one message are carried in
  order.

  An image inside a **tool result** still degrades to a text placeholder on
  the HTTP and gateway drivers: a tool message is text-only in those dialects,
  so there is nowhere to put it. Converse carries it, and always did.

### Patch Changes

- 935b8f3: Stop dumping a tool result's base64 payload into the prompt.

  Four drivers mishandled a tool result carrying content blocks, each in its
  own way: bedrock and the http driver's anthropic dialect `JSON.stringify`d
  the whole array, putting a screenshot's base64 into the prompt as JSON
  text; openrouter and the http driver's openai dialect passed the array
  through raw to an endpoint expecting a string; lmstudio folded it into a
  template literal, producing `[object Object]` per block. The model cannot
  decode any of it, and it costs a fortune in tokens.

  - The three text-only wires now flatten with the SDK's existing helper,
    which names a non-text block and its size instead of inlining it. The
    openai and ollama drivers already did this — the helper was there and
    four callers were missing.
  - **bedrock sends the image as an image.** That wire carries images
    natively, so a placeholder would be a downgrade the other drivers accept
    only because their format has no room for one. Text and image survive as
    separate blocks in order, and a media type the format does not accept
    still degrades to a named placeholder rather than being smuggled through
    as text.

- 935b8f3: A turn that asked for tools no longer ends because the provider said it
  didn't.

  The iteration loop ended the turn on `finishReason === 'stop'` **before**
  looking at whether the model had asked for tools. Endpoints on the OpenAI
  wire shape — gateways and local servers especially — routinely report `stop`
  on the same response that carries a populated `tool_calls`, and three of
  this repo's drivers passed that value straight through.

  The damage was total and silent: every requested call skipped, an assistant
  turn left carrying `tool_use` blocks nothing ever answered, and the run
  settling as though it had finished the work it never started.

  - **The runtime now treats tool calls as the fact and the finish reason as
    the summary.** When they disagree, the calls win. This is the load-bearing
    fix: it protects every driver, including ones this repo does not ship.
  - **The three drivers that cast the reason raw now report it honestly** —
    a stream that produced a tool call reports `tool_calls`, whatever the
    endpoint called it. Defence in depth, and it makes the reported reason
    true for anyone else reading it.

  The existing suite could not catch this: the scripted mock reports
  `tool_calls` whenever it emits one, which is what an honest provider does
  and therefore never the case that breaks.

- 935b8f3: A user message can carry a document

  Documents existed in the type system only in the tool-result direction, and both first-party drivers mapped images only on the input side. So "here is the contract, answer questions about it" — a mainstream workload — was reachable only by having a tool read the file and stringify it. That loses the provider's native document handling (page structure, built-in OCR, citations) and pays the text cost instead.

  `UserMessage.attachments` is now `MessageAttachment[]`: an image or a document. The discriminant is optional and stays optional — an attachment without one is an image, which is what every attachment was before, so no existing caller changes.

  `supportsDocuments` sits beside `supportsVision` in the driver capability declaration, and the runtime checks it the same way: a document sent to a driver that declares `false` warns before the request, or throws under `strictCapabilities`, instead of letting the model answer about a file it never saw. The two are counted separately because they are separate wire shapes and a driver can map one without the other.

  The two first-party drivers map documents natively. The remaining five map images only and now say so; a document reaching them degrades to a named placeholder that says which kind was dropped, rather than one that calls a document an image.

## 1.0.3

### Patch Changes

- 3fd2524: Normalize request-start and mid-stream failures across all seven provider
  drivers with the new public `ProviderRequestError` taxonomy. Errors expose
  `kind` (`throttle`, `network`, `auth`, `context_overflow`, `bad_request`, or
  `server`), `providerId`, and optional `status` / `retryAfterMs`, with
  `isProviderRequestError` available for structural narrowing across package
  copies.

  Provider error messages and metadata deliberately omit vendor response bodies,
  URLs, messages, and causes because upstream errors can echo credentials. HTTP
  dialect-mismatch diagnostics now keep only the endpoint origin and status.
  Caller-owned aborts remain unchanged instead of being reclassified.

  The runtime preserves the classified error through streaming and publishes its
  safe metadata as `Run.lastProviderError` and
  `run_failed.providerError`. Bedrock stream-exception events and provider
  iterator/SSE failures no longer appear as clean end-of-stream.

  `retryAfterMs` is metadata only; this change does not add retries or alter vendor
  SDK retry settings. Provider packages now require `@namzu/sdk >=1.3.0`, the
  first SDK release containing these runtime helpers and types.

  Ollama now maps `done_reason: "length"` truthfully so runtime continuation can
  run. LM Studio treats content-free `contextLengthReached` as context overflow,
  while preserving `"length"` after partial content, and creates its WebSocket
  client lazily on first use.

## 1.0.2

### Patch Changes

- f1f000c: Declare honest driver capabilities on each provider instance.

  Every shipped driver now exposes `readonly capabilities` (and
  re-exports its `*_CAPABILITIES` constant from the client module)
  describing what the DRIVER does — not what the vendor API could do —
  so the SDK's capability negotiation can warn instead of silently
  degrading:

  - `@namzu/ollama`: `supportsTools: false`, `supportsVision: false`
    (the driver never sends tool schemas and drops image attachments).
  - `@namzu/lmstudio`: `supportsTools` corrected `true` → `false` and
    `supportsFunctionCalling` `true` → `false` — the driver folds tool
    messages into user text and never sends tool schemas;
    `supportsVision: false`.
  - `@namzu/anthropic`: full (`supportsVision: true` — image attachments
    already mapped).
  - `@namzu/bedrock`, `@namzu/openrouter`, `@namzu/http`: tools pass
    through (`supportsTools: true`) but `supportsVision: false` until
    their message translation maps attachments.

## 1.0.1

### Patch Changes

- 9df35d1: Make a Stop abort the IN-FLIGHT model turn, not only between turns.

  `ChatCompletionParams` gains an optional `signal?: AbortSignal`. The query
  runtime threads the run's abort signal into every provider call (the streaming
  turn and the forced-final summary) and now drives the provider stream through a
  MANUAL iterator that RACES each `next()` against the abort — so a cancellation
  tears the turn down within a tick even if a transport buffers or ignores the
  signal, with the abort propagating out of the generator so the run settles as
  `cancelled`. The stream consumer cleans up on every exit (removes the abort
  listener, calls `iterator.return()`), and the natural-completion break
  re-checks the signal so a Stop that lands exactly as the turn finishes is
  recorded as cancelled rather than a normal end-of-turn.

  Every provider now honours the signal at the transport: Anthropic
  (`messages.create({ signal })`), OpenAI (`create(..., { signal })`), Bedrock
  (`send(..., { abortSignal })`), OpenRouter + HTTP (compose with the request
  timeout via `AbortSignal.any`), Ollama (the returned iterator's `.abort()`),
  and LM Studio (`respond(..., { signal })` → the SDK's websocket cancel) — each
  plus a cheap per-chunk `signal.throwIfAborted()` for promptness.

  Fully additive and inert when unset: a never-aborted signal is behaviourally
  identical to omitting it, so existing callers and uncancelled runs are
  byte-identical.

## 1.0.0

### Patch Changes

- Updated dependencies [542f057]
- Updated dependencies [df09910]
- Updated dependencies [140bcc0]
- Updated dependencies [ea21863]
- Updated dependencies [38c4b62]
- Updated dependencies [265150b]
- Updated dependencies [a1c6694]
- Updated dependencies [52af97e]
- Updated dependencies [a71422a]
- Updated dependencies [d6b5bc1]
- Updated dependencies [8fd9349]
- Updated dependencies [63e44f7]
- Updated dependencies [63b4885]
- Updated dependencies [38c4b62]
- Updated dependencies [6b74cd0]
- Updated dependencies [d86b161]
  - @namzu/sdk@1.0.0

## 0.2.0

### Minor Changes

- 2749d32: RunEvent v3 + streaming-only `LLMProvider` (ses_001-tool-stream-events).

  The kernel now emits a per-message and per-tool-input lifecycle on the
  event bus, and the provider contract collapses to a single streaming
  entry point. Together these unlock live tool-call rendering (Calling →
  Running → Done with incremental input) for SSE consumers — the cowork
  workspace surface that motivated the work in the first place.

  ## Breaking changes

  ### `LLMProvider.chat()` removed

  `LLMProvider` exposes a single LLM entry point: `chatStream()`. The
  non-streaming `chat()` method is gone from every shipped provider
  (`@namzu/anthropic`, `@namzu/openai`, `@namzu/bedrock`,
  `@namzu/openrouter`, `@namzu/http`, `@namzu/ollama`,
  `@namzu/lmstudio`).

  Consumers that need an aggregated `ChatCompletionResponse` use the new
  helper:

  ```ts
  import { collect } from "@namzu/sdk";

  const response = await collect(provider.chatStream(params));
  ```

  `collect()` drains the stream and assembles the legacy response shape:
  text concatenated in arrival order, tool calls bucketed by index,
  latest `finishReason` and `usage` win, defaults to `{ finishReason:
'stop', zero usage }` when the provider omits them (defensive against
  SDK quirks like dropped `message_stop` frames).

  The orchestrator consumes the stream directly so it can emit per-delta
  RunEvents — it does NOT call `collect()`.

  ### `RunEvent` envelope `schemaVersion: 2 → 3`

  `RUN_EVENT_SCHEMA_VERSION` is now `3`. The envelope narrows from `2 |
3` to `3`; sub-session lifecycle events stamp `3` automatically via
  `RunEventSchemaVersion`.

  ### `llm_response` removed

  The coarse `llm_response` event is replaced by a message lifecycle:

  - `message_started { runId, iteration, messageId }` — first chunk arrives.
  - `text_delta { runId, iteration, messageId, text }` — per-chunk text.
  - `message_completed { runId, iteration, messageId, stopReason, usage?, content? }` — provider stream closes.

  `message_completed.content` is the aggregated text and is optional —
  consumers that already accumulate `text_delta` themselves can ignore
  it; consumers that only care about the completed message (telemetry,
  A2A bridge) read it directly.

  `stopReason` is the new `MessageStopReason` union: `'end_turn' |
'tool_use' | 'max_tokens' | 'stop_sequence' | 'pause_turn' | 'refusal'
| 'forced_finalize'`.

  ### Tool input lifecycle

  Tool calls now traverse a five-event lifecycle keyed by `toolUseId`:

  - `tool_input_started { runId, iteration, messageId, toolUseId, toolName }`
  - `tool_input_delta { runId, toolUseId, partialJson }` — raw fragment
  - `tool_input_completed { runId, toolUseId, input }` — parsed object
  - `tool_executing { runId, toolUseId, toolName, input }` — runtime invokes
  - `tool_completed { runId, toolUseId, toolName, result, isError }` — required `isError`

  `tool_executing` and `tool_completed` payloads tighten: `toolUseId`
  becomes required on both, `isError` becomes required on
  `tool_completed`. The wire-level `tool.error` event is dropped — the
  boolean carries the same signal without ambiguity.

  Probe veto, malformed JSON args, plugin hook errors, and exception
  throws inside `tools.execute()` all now emit a terminal
  `tool_completed { isError: true }` so consumer UI cards can finalise
  instead of being orphaned.

  ### Ephemeral events skip persistence

  `text_delta` and `tool_input_delta` are flagged `isEphemeralEvent()`
  and bypass `transcript.jsonl`. They live only on the in-memory bus
  for live UI rendering. Replay is unaffected (it reads checkpoints,
  not transcripts). The bus has a 1000-event soft cap; under pressure
  the oldest ephemeral is dropped while lifecycle events are preserved.

  ### `StreamChunk.delta.toolCallEnd`

  New optional field signalling per-tool-block boundary closure. The
  orchestrator translates it into `tool_input_completed`. Providers
  that emit a per-tool-block close (Anthropic `content_block_stop` of
  type `tool_use`, Bedrock equivalent) populate it; providers that
  don't fall back to end-of-stream flushing.

  ## Migration

  Most consumers only use the iteration orchestrator's emitted
  `RunEvent` stream. They:

  1. Replace `case 'llm_response':` handlers with a `case
'message_completed':` handler reading `event.content`.
  2. Drop any reads of `event.hasToolCalls` — derive from the
     subsequent absence/presence of `tool_executing` events keyed by
     the same `runId`.
  3. Optional: subscribe to `text_delta` and `tool_input_*` for live
     rendering. The events are interleaved by `toolUseId` to support
     parallel tool calls.

  Consumers calling `provider.chat()` directly:

  ```diff
  - const response = await provider.chat(params)
  + import { collect } from '@namzu/sdk'
  + const response = await collect(provider.chatStream(params))
  ```

  Aggregated response shape is identical.

  ## Internal surface (not externally consumed)

  - `runtime/query/iteration/index.ts` — new `streamProviderTurn()`
    helper, replaces synthesised `message_started`/`message_completed`
    with native streaming. `forced_finalize` path uses `collect()`.
  - `provider/instrumentation.ts` — captures `usage` from the last
    chunk that supplies one (`extractStreamUsage`).
  - `runtime/query/events.ts` — `EventTranslator.emitEvent` skips
    `appendEvent()` for ephemeral events, applies the queue cap.
  - `bridge/sse/mapper.ts` — six new wire types
    (`message.created/delta/completed`,
    `tool.input_started/delta/completed`); `tool.error` removed;
    `tool.executing/completed` carry `tool_use_id` + `is_error`.
  - `bridge/a2a/mapper.ts` — `message_completed.content` routes to A2A
    status update, replacing the per-iteration `llm_response` mapping.

  ## Tests

  SDK suite at 958 (was 943 + new contracts − removed
  `chat()`/`llm_response` invariants). `pnpm typecheck && pnpm lint &&
pnpm test && pnpm build` all green across every package. The
  `@namzu/http` request-construction and response-parsing suites have
  10 tests marked `.skip` pending an SSE-mock rewrite — the streaming
  path is still covered by the existing streaming-suite tests.

## 0.1.2

### Patch Changes

- c9b180d: Coordinated patch bump across all publishable packages after the `@namzu/telemetry@0.1.0` extraction landed. No functional changes — this is a compatibility and release-pipeline validation cut to (a) exercise the Trusted Publisher binding for `@namzu/telemetry` that was configured after the 0.1.0 bootstrap publish, and (b) give consumers a single aligned set of patch versions that all know about the new telemetry package.

  Resulting versions:

  - `@namzu/sdk` → `0.4.1`
  - `@namzu/telemetry` → `0.1.1`
  - `@namzu/computer-use` → `0.2.1`
  - `@namzu/anthropic`, `@namzu/bedrock`, `@namzu/http`, `@namzu/lmstudio`, `@namzu/ollama`, `@namzu/openai`, `@namzu/openrouter` → `0.1.2`

## 0.1.1

### Patch Changes

- 40eb841: Widen `@namzu/sdk` peer range to `>=0.1.6 <1.0.0`.

  The previous peer range `^1 || ^0.1.6` resolved to `>=0.1.6 <0.2.0 || >=1.0.0`, which excluded the published `@namzu/sdk@0.2.0` and caused `npm install @namzu/sdk @namzu/<provider>` to fail with ERESOLVE on a clean machine. The new range covers every pre-1.0 SDK minor from 0.1.6 onward; the 1.0 pledge will be the next explicit widening.

  This is the first release under the new Changesets-driven workflow and the wide-pre-1.0-peer convention. Consumers who followed the README's "getting started" install were previously blocked; after this release `npm install @namzu/sdk@latest @namzu/<provider>@latest` resolves cleanly.

All notable changes to `@namzu/openrouter` are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.0] — 2026-04-15

### Added

- Initial release. OpenRouterProvider extracted from @namzu/sdk core per ADR-0001.
- OpenAI-compatible Chat Completions API (chat + chatStream) via native fetch.
- Tool-use + function calling support.
- `registerOpenRouter()` helper for one-call provider registration.
- Module augmentation of @namzu/sdk's ProviderConfigRegistry for type-safe config.

### Changed

- Observability (OTEL spans, structured logging) removed pending @namzu/telemetry package.
