Releases: baabakk/llm-ports
Release list
@llm-ports/capabilities v0.1.0-alpha.26.1 — Internal migration hotfix
@llm-ports/capabilities v0.1.0-alpha.26.1 — Internal migration hotfix
Released 2026-07-03. Install: pnpm add @llm-ports/capabilities@alpha
Hotfix for a gap in the alpha.26 ship. Consumer impact: zero API changes; upgrade silently unblocks the alpha.27 path.
The gap
The alpha.26 ship marked {instructions, prompt} deprecated on the port interface but left @llm-ports/capabilities calling the port with the deprecated shape internally. That worked at runtime (the Registry's dual-population synthesized messages from the legacy fields) but would have failed to compile against @llm-ports/core@alpha.27 once the legacy fields are removed.
Every downstream consumer using createExtractor / createClassifier / createScorer / createSummarizer / createDrafter / createAnalyzer / createPlanner would have broken at alpha.27.
Caught during a downstream migration review by a consumer paying attention to the upstream call sites. Filed immediately.
The fix
All 7 factory implementations now build messages: LLMMessage[] via toMessages(system, userPrompt) and pass that to the port instead of {instructions, prompt}.
Files updated:
src/understanding/classify.tssrc/understanding/extract.tssrc/understanding/score.tssrc/reasoning/analyze.tssrc/reasoning/plan.tssrc/generation/draft.tssrc/compression/summarize.ts
Regression guard
New test suite tests/legacy-shape-guard.test.ts uses a recording spy port that asserts every factory calls .generateStructured / .generateText with messages set and NEITHER instructions nor prompt set. A future PR reintroducing the legacy shape in an internal port call trips this test before publish.
Test coverage
- 7 new legacy-shape-guard tests (one per factory)
- 4 existing test files updated to read from
messagesvia newgetSystemContent()/getUserContent()helpers instead of the removedoptions.prompt/options.instructionsfields - 888 tests pass across the workspace (was 881 at alpha.26; +7 new; 0 regressions)
Consumer impact
- Zero API changes. Wrapper input types (
DraftInput.instructions,ClassifyInput.contextOverride, etc.) remain unchanged. That's BEPA-facing consumer surface, not the port surface. - Only the internal port call shape changed. Consumers using the factories continue to work exactly as before.
- Upgrade path. Bump
@llm-ports/capabilitiesfromalpha.26toalpha.26.1(or leave^alphaand let npm resolve). No code changes required.
Why alpha.26.1 (not alpha.27)
Alpha.27 is reserved for the coordinated removal of the deprecated instructions / prompt fields from @llm-ports/core and all adapters (planned for ~2026-07-16). This is a hotfix on the alpha.26 line to unblock that coordinated ship. Only @llm-ports/capabilities bumps to alpha.26.1; the other 6 packages stay on alpha.26.
Credit
Thanks to the BEPA migration reviewer (2026-07-03) for catching the upstream factory quirk during their alpha.20.1 → alpha.26 audit and calling it out before it became a ship-blocker.
@llm-ports v0.1.0-alpha.26 — API Unification (Canonical Messages Input)
@llm-ports v0.1.0-alpha.26 — API Unification (Canonical Messages Input)
Released 2026-07-02. Install: pnpm add @llm-ports/core@alpha @llm-ports/adapter-openai@alpha
toMessages(instructions, prompt).
The unification
Every provider's actual API speaks messages: Message[] natively. The port's { instructions, prompt } shape was a defensible compression for single-turn calls but couldn't model multi-turn workloads (chat, interview agents, coaching workflows) without bad workarounds.
Alpha.26 unifies. All four generation methods now accept a canonical messages: LLMMessage[] input, aligning with runAgent's existing shape and every provider's native protocol:
// Before (alpha.25 and earlier)
port.generateText({
taskType: "triage",
instructions: SYSTEM_PROMPT,
prompt: userInput,
});
// After (alpha.26 mechanical, via shim — one-line change per site)
import { toMessages } from "@llm-ports/core";
port.generateText({
taskType: "triage",
messages: toMessages(SYSTEM_PROMPT, userInput),
});
// After (alpha.26 idiomatic, via helpers)
import { sys, usr } from "@llm-ports/core";
port.generateText({
taskType: "triage",
messages: [sys(SYSTEM_PROMPT), usr(userInput)],
});
// After (alpha.26 native multi-turn — previously unavailable)
port.generateStructured({
taskType: "interview-turn",
schema: InterviewTurnSchema,
messages: conversationHistory, // full context with alternating roles
});What ships
1. New canonical field: messages: LLMMessage[]
On all four generation methods (generateText, generateStructured, streamText, streamStructured). Supports arbitrary multi-turn shapes.
2. Migration shim + convenience helpers
New exports from @llm-ports/core:
toMessages(instructions?, prompt): LLMMessage[]— one-line migration for the legacy shape.sys(content: string): LLMMessage— idiomatic system message constructor.usr(content: MessageContent): LLMMessage— idiomatic user message constructor.
3. Four new errors
MessagesRequiredError— neithermessagesnorpromptsupplied.EmptyMessagesError—messagesarray is empty.MessagesConflictError— bothmessagesAND legacy fields supplied (ambiguity is a caller bug).PromptRequiredError—toMessages()called with no prompt.
4. Deprecation warning UX
Single-line console.warn per method per Registry when the legacy shape is used. Method-only dedup — a consumer with 50 legacy call sites across all four methods gets 4 warnings total (one per method), enough signal to trigger a migration audit without flooding logs.
Opt out for mid-migration:
const registry = createRegistryFromEnv({
suppressDeprecationWarnings: true, // alpha.26+; removed in alpha.27
});Structured logging:
const registry = createRegistryFromEnv({
deprecationWarningHandler: (msg) => logger.warn({ deprecation: true, msg }),
});5. Registry-side dual-population
The RegistryPort normalizes both shapes to canonical messages before dispatch AND populates the legacy instructions + prompt fields from the resolved messages. This means:
- Adapters updated for
messages(adapter-openai in this release) get the full multi-turn path. - Adapters not yet updated (adapter-anthropic, adapter-google, adapter-ollama, adapter-vercel) continue to work by reading the synthesized legacy fields. Single-turn works fully; multi-turn semantically degrades to "system + last user message" until each adapter is refactored in a patch release.
What's NOT changing
runAgentalready tookmessages. Zero migration impact if you only userunAgent.- All other options (temperature, refs, budgetScope, cacheControl, providerExtras, signal, forceProviderAlias, reasoningEffort, strict): unchanged.
- Error taxonomy, observability hooks, budget gating, per-attempt timeout, task routing, fallback chain: unchanged.
- The
MessageContenttype (per-turn content:string | ContentBlock[]): unchanged.
Timeline
- alpha.26 (this release): both shapes work. Deprecation warnings on legacy.
- alpha.27 (~2 weeks): legacy fields removed. TypeScript compilation error if consumers haven't migrated.
Test coverage
- 10 helper + shim tests (toMessages, sys, usr, error paths)
- 5 canonical messages-flow tests (Registry → adapter passing verbatim)
- 4 legacy-path tests (deprecation warning fires, dedups, respects suppression)
- 8 error-path tests (all four new errors)
- All existing alpha.25 tests continue to pass unchanged
881 total tests pass across the workspace (was 864 at alpha.25; +17; zero regressions).
Adapter status
| Adapter | messages input | Multi-turn |
|---|---|---|
adapter-openai |
✅ | ✅ Full |
adapter-anthropic |
⚠ | ⚠ Single-turn only (patch release upcoming) |
adapter-google |
⚠ | ⚠ Single-turn only (patch release upcoming) |
adapter-ollama |
⚠ | ⚠ Single-turn only (patch release upcoming) |
adapter-vercel |
⚠ | ⚠ Single-turn only (patch release upcoming) |
Consumers using multi-turn messages today should route through adapter-openai. Other adapters accept messages shape (Registry-normalized) but internally use the last user message; full multi-turn support follows in patch releases per adapter.
Alternatives considered and rejected
- Parallel field with XOR semantics (two-ways-to-do-one-thing). Rejected.
- New
chatmethod (grows port surface). Rejected. - Rename
prompttoinputwith union type (runtime discrimination churn). Rejected. - Extend
MessageContentto includeLLMMessage[](conflates content vs sequence). Rejected. - Defer to beta.0. Considered; rejected. Beta.0 is the stability signal — if we're breaking, we should break in alpha.
- Do nothing. Rejected. The abstraction fails its job if it refuses to model a use case the protocol supports natively.
Full release notes | alpha.25 → alpha.26 migration guide | alpha.26 planning discussion
@llm-ports v0.1.0-alpha.25 — Observability surface + reliability hardening
@llm-ports v0.1.0-alpha.25 — Observability surface + reliability hardening
Released 2026-07-02. Install: pnpm add @llm-ports/core@alpha @llm-ports/adapter-openai@alpha
Three additive features under one release theme. Zero breaking changes. All existing code compiles and runs unmodified.
What's in this release
1. refs?: Record<string, ArtifactRef> on every call (#53)
A consumer-owned, keyed map of artifact references that flows through to every observability event (onCost, onTokenUsage, onFallback, onCacheHit, onValidationRetry) unchanged. Use it for prompt versioning, cost attribution by tenant / project / experiment, session correlation — any versioned or identifiable artifact you want stamped onto trace.
import type { ArtifactRef } from "@llm-ports/core";
port.generateStructured({
taskType: "extract",
prompt: input,
schema: MySchema,
refs: {
prompt: { key: "extractor-v3", version: 3, hash: "sha256:..." },
tenant: { key: "acme-corp" },
session: { key: "sess-abc123" },
},
});The observability side reads them back cleanly:
const registry = createRegistryFromEnv({
observability: {
onCost: (e) => audit.recordCost({
totalUsd: e.totalUsd,
promptVersion: e.refs?.prompt?.version,
tenant: e.refs?.tenant?.key,
}),
},
});Non-goals (guarded against scope creep): not validated, not sent to the model, not read by adapters, no vocabulary standardization, no merging or inheritance across nested runAgent calls. Pure consumer-owned trace metadata.
2. runtimeFallback: "aggressive" preset (#54, LP-REQ-01)
Three consumers (BEPA Plan 29, HomeSignal, SalesCoach Plan 30) each rebuilt the same classifier by hand. Bundled as a preset:
const registry = createRegistryFromEnv({
adapters: { /* ... */ },
runtimeFallback: "aggressive",
});Walks on (in addition to the default ProviderUnavailableError):
RateLimitError— try next provider rather than wait out backoffEmptyResponseError— adapter's own retries gave upContextWindowExceededError— try a larger-window providerBadRequestErrormatching credit-exhaustion body patterns (credit balance is too low,insufficient funds,account disabled,billing,exceeded your current quota,out of credits,payment required,organization has been deactivated)- Raw error with
status >= 500(defensive)
Does NOT walk on AuthenticationError, generic BadRequestError (malformed request), ContentPolicyViolationError, BudgetExceededError, SessionBudgetExceededError.
Classifier + credit-pattern list exported for composition:
import { aggressiveShouldFallback, AGGRESSIVE_CREDIT_EXHAUSTION_PATTERNS } from "@llm-ports/core";3. Streamed cost surfacing (#55)
onCost and onTokenUsage now fire once per stream at natural completion for streamText and streamStructured — matching the non-streaming contract. Enabled by default in adapter-openai via stream_options: { include_usage: true }.
for await (const chunk of registry.getPort().streamText({
taskType: "chat",
prompt: "hello",
refs: { session: { key: "sess-abc123" } },
})) {
ui.append(chunk);
}
// onCost + onTokenUsage fired once at completion with refs.session.key preserved.Semantics:
- Emit ONCE per stream, at natural completion.
- Mid-stream errors do NOT emit.
- Consumer-cancelled streams (via
AbortSignal) do NOT emit. - Opt out per adapter with
createOpenAIAdapter({ streamUsage: false })for compat providers that rejectstream_options.
Adapter coverage: adapter-openai in this release. Other adapters (adapter-anthropic, adapter-google, adapter-ollama, adapter-vercel) follow in patch releases as their SDK's stream-completion metadata surface is wired.
Backwards compatibility
All three features are opt-in. Existing code compiles and runs unchanged. No new imports required unless you want to use the new features.
Tests
- 8 refs tests
- 23 aggressive-fallback tests (positive + negative per error class + Registry integration)
- 5 streamed-cost tests (callback firing, no-op path, mid-stream error path, refs preservation, streamStructured parity)
- 864 total (was 828; +36; 0 regressions)
⚠️ What's next: alpha.26 is BREAKING
The next release will be a BREAKING API unification to align the port with every provider's native protocol. The four generation methods (generateText / generateStructured / streamText / streamStructured) will move from { instructions, prompt } to a canonical messages: LLMMessage[] input.
Why: consumers with multi-turn workloads (chat, interview agents, coaching workflows) currently have three bad workarounds — roll conversation history into a string (loses role fidelity), abuse runAgent with an empty tools object (semantically broken), or reach past the port via providerExtras (bypasses the abstraction). None are acceptable. Aligning the port with the underlying protocol (which every provider speaks natively) fixes this and matches runAgent's existing shape.
Migration path:
- alpha.26: ships
{ messages, ... }alongside{ instructions, prompt, ... }. Existing fields marked@deprecated. Deprecation warnings emit with fingerprint dedup (one warning per unique call site per Registry). A one-line migration shim ships:toMessages(instructions, prompt)returns the equivalent messages array. Plus convenience helperssys(content)andusr(content). - alpha.27 (~2 weeks later): removes the deprecated fields. TypeScript compilation error if consumers haven't migrated.
Migration examples (planned):
// alpha.25 (current)
port.generateText({
taskType: "triage",
instructions: SYSTEM_PROMPT,
prompt: userInput,
});
// alpha.26 mechanical (via shim, one-line change)
port.generateText({
taskType: "triage",
messages: toMessages(SYSTEM_PROMPT, userInput),
});
// alpha.26 idiomatic (via helpers)
port.generateText({
taskType: "triage",
messages: [sys(SYSTEM_PROMPT), usr(userInput)],
});
// alpha.26 native (multi-turn, previously unavailable)
port.generateStructured({
taskType: "interview-turn",
schema: InterviewTurnSchema,
messages: conversationHistory, // full context; no workaround needed
});Full plan and open design questions in the alpha.26 planning discussion. If you have a workload that would be affected, comment there — feedback informs the deprecation window and shim shape.
Full release notes | alpha.24 → alpha.25 migration guide | Observability concept
v0.1.0-alpha.24 — Catalog architectural redesign + onValidationRetry emission + Cerebras pricing
The headline
The static catalog is now FROZEN. New reasoning models are caught by:
- Runtime detection (alpha.22+; universal correctness path; no maintenance)
- Behavioral fingerprint cache (NEW in alpha.24; opt-in optimization)
The KNOWN_REASONING_MODELS catalog stays for the stable well-known cases (OpenAI o-series, gpt-5-nano, gpt-oss family, Qwen3.6, MiniMax-M2.7, MiMo-V) but stops growing. See the three-tier architecture doc.
This answers Babak's architectural pushback across the alpha.22 → alpha.23 transition that maintaining per-(model × provider) regex patterns is unsustainable. The empirical survey at docs/research/reasoning-models-survey-2026-06.md identified 30+ reasoning models across 5 providers — none of them need catalog entries from alpha.24 onward.
What's in this release
Behavioral fingerprinting (adapter-openai)
Opt-in cross-process cache that eliminates the first-call discovery penalty:
```ts
import { createOpenAIAdapter, FileFingerprintCache } from "@llm-ports/adapter-openai";
const adapter = createOpenAIAdapter({
apiKey: process.env.DEEPINFRA_API_KEY!,
baseURL: "https://api.deepinfra.com/v1/openai\",
fingerprintCache: new FileFingerprintCache("~/.llm-ports/fingerprints.json"),
});
```
Bundled backends: `InMemoryFingerprintCache`, `FileFingerprintCache`. Bring-your-own backend (Redis, S3, KV) via the `FingerprintCacheBackend` interface. Standalone helper `fingerprintModel()` for CI warm-starts.
The fingerprint analyzer detects all four CoT field conventions in the OpenAI-compat ecosystem:
- `usage.completion_tokens_details.reasoning_tokens` (OpenAI native)
- `message.reasoning` (Cerebras, Groq, SambaNova)
- `message.reasoning_content` (DeepInfra, Parasail)
- inline `...` (legacy R1 distills)
Every successful response is inspected for free — no extra probe call needed.
`onValidationRetry` Registry-level emission (core)
Closes the alpha.21-deferred Registry-level emission:
```ts
import { deriveValidationRetryFromAdapterRetry } from "@llm-ports/core";
const adapter = createOpenAIAdapter({
apiKey: process.env.OPENAI_API_KEY!,
onRetry: deriveValidationRetryFromAdapterRetry(registry),
});
```
Filters adapter retry events for `reason === "validation-feedback"` and forwards to the Registry's `observability.onValidationRetry` hook. Optionally chains with a user-supplied adapter-level callback.
Cerebras pricing entries
Two production models added to `OPENAI_PRICING`:
| Model | Input $/1M | Output $/1M | Confidence |
|---|---|---|---|
| `gpt-oss-120b` | $0.35 | $0.75 | HIGH (primary docs) |
| `zai-glm-4.7` | $2.25 | $2.75 | MEDIUM (third-party only) |
Research artifact
docs/research/reasoning-models-survey-2026-06.md — empirical catalog across DeepInfra, Parasail, SambaNova, Cerebras, Groq. 30+ reasoning models documented with response shapes, pricing, finish_reason behavior, and round-trip incompatibility caveats.
Tests
- 25 fingerprinting tests (analyzer + 2 backends + adapter integration + error swallowing)
- 11 validation-retry derivation tests
- 2 Cerebras pricing tests
- 828 total (was 792 in alpha.23; +36 new, 0 regressions across the other 6 packages)
Backwards compatibility
All changes additive. When `fingerprintCache` is undefined (default), the adapter behaves identically to alpha.23. Existing onRetry filters keep working unchanged. The Registry's observability.onValidationRetry hook (type-only since alpha.21) now optionally fires via the helper.
Empirical sources
- Babak's architectural pushback across the alpha.22 → alpha.23 transition that the static catalog is unsustainable
- 2026-06-24 research survey (10 parallel agents) covering DeepInfra, Parasail, SambaNova, Cerebras, Groq
- ADW's 2026-06-19 production diagnostic confirming the failure mode (silent first-call penalty on unrecognized reasoning models)
Install
`pnpm add @llm-ports/core@alpha @llm-ports/adapter-openai@alpha`
All 7 publishable packages bumped to 0.1.0-alpha.24.
What's NOT in this release (deferred)
- Streamed cost surfacing for `onCost` / `onTokenUsage` — complex semantics (when does a stream complete?); deserves its own release
- LP-REQ-01 resilient fallback preset — no production blocker
- `llm-ports` meta-package (unscoped npm landing page) — belongs in v0.1.0 GA prep
v0.1.0-alpha.23 — Agentic tool-use rescue paths + per-attempt timeout (additive)
What's in this release
Four additions targeting the agentic tool-use failure modes ADW's 2026-06-19 multi-team build-loop diagnostic surfaced. All additive, no breaking changes.
ASK 1 — Harmony tool-call extraction (adapter-openai)
parseHarmonyToolCalls() extracts one or more tool calls from a harmony-formatted message.reasoning_content string. When the standard tool_calls array is empty AND reasoning_content contains a parseable harmony tool call, the call is hoisted into the executable path. Zero extra LLM calls.
Closes the DeepInfra gpt-oss harmony tool-use gap that alpha.22 left open. Pre-alpha.23, runAgent treated harmony tool intent in reasoning_content as an empty assistant turn and terminated. Post-alpha.23, the tool call executes the same as a standard one.
The parser returns null gracefully when reasoning_content is empty, prose chain-of-thought, bare JSON without a tool name (the empirical {"path":"","depth":3} probe case), or contains malformed JSON. The zero-tool-call rescue (ASK 2) handles the prose-only case via a corrective retry.
ASK 2 — Zero-tool-call corrective rescue (adapter-openai)
When the model returns a clean completion (finish_reason: "stop" or "length") with prose content, empty tool_calls, and the request had a tools array — the adapter retries once with a corrective system message asking it to use the standard tool_calls format.
Closes the mimo-parasail prose case from ADW's diagnostic. Pre-alpha.23, mimo returned ~69 tokens of prose with zero tool_calls, runAgent terminated as completed, ADW promoted empty stubs to main. Post-alpha.23, the rescue gives the model one corrective shot before termination.
Five discriminators prevent over-firing: no tools in request → skip; tool_calls populated → skip; empty content → reasoning starvation (handled by alpha.22 path); reasoning_content populated → harmony case (handled by ASK 1); conversation includes a tool role message → model is summarizing, not failing → skip.
Single-shot retry only. If the rescue also returns prose, the consumer's orchestration is responsible (e.g., "planned ≠ written" guard at the workflow level).
ASK 3 — Telemetry tags (core)
Two new discriminators on the existing RetryReason union:
\"harmony-tool-call-extracted\"— adapter recovered a tool call from harmony channel (no retry; observability only)\"zero-tool-call-prose-retry\"— adapter retried with corrective system message
Consumers filter the existing adapter onRetry hook on these values to distinguish "was rescued via X" from "clean zero-output (failover candidate)."
Per-attempt timeout helper (core)
RegistryOptions.perAttemptTimeoutMs wraps every provider attempt inside walkChain with an AbortController + timer. On timeout, the abort propagates to the adapter; the adapter throws ProviderUnavailableError; the Registry's shouldFallback catches it and walks to the next provider with a fresh timer.
const registry = createRegistryFromEnv({
env: process.env,
adapters: { /* ... */ },
perAttemptTimeoutMs: 30000, // 30s cap per provider
});Per-attempt, not chain-wide. Each provider gets its own budget. Composes with user-supplied signal — both fire the same wrapped controller; shorter trigger wins.
Closes the ADW production wedge from 2026-06-19T15:40 UTC where mimo-parasail hit reasoning-starvation, retry expanded budget, then silently grinded for 3+ minutes with no timeout/failover. The AbortSignal infrastructure was already in place; this helper makes the per-attempt timeout pattern ergonomic.
What this does NOT fix
The Case B "under-production" pattern (model makes some tool calls then stops with the planned manifest incomplete) is NOT addressed at the adapter layer. The adapter sees a clean multi-call completion; only the orchestration knows the manifest is incomplete. ADW (and similar agentic orchestrators) should add a "planned ≠ written" guard at the workflow layer.
Backwards compatibility
All four additions are additive. Existing callers see no API surface changes. Two existing tests in adapter-openai (tool-schema-conversion, tool-use-edges) were updated to use mockResolvedValue instead of mockResolvedValueOnce so ASK 2's rescue retry has a target. Test intent (schema conversion correctness; termination logic) preserved.
Tests
- 13 new harmony extraction tests (all parser branches + runAgent integration + telemetry emission)
- 8 new zero-tool-call rescue tests (rescue fires correctly + 5 discriminator regression guards + single-shot guarantee + telemetry)
- 8 new per-attempt timeout tests (hanging provider falls back, fast passes through, no timeout = backwards-compat, user signal composes, NOT chain-wide, onFallback emission, public field exposure)
Total: 792 unit tests pass (was 763 in alpha.22; +29, 0 regressions in any other suite).
Empirical sources
- ADW Development_Logs.md commit b1eeee2 — DeepInfra harmony tool-use diagnostic
- ADW production wedge incident 2026-06-19T15:40 UTC — mimo silent prose-only completion
- Babak's raw 2-turn DeepInfra probe — empirical evidence of the
reasoning_contentshape - llm-ports#46 / discussion #50 — design discussion + ADW dev critique
Install
`pnpm add @llm-ports/core@alpha @llm-ports/adapter-openai@alpha`
All 7 publishable packages bumped to 0.1.0-alpha.23 (the unchanged adapters are revved to keep the @Alpha tag synchronized across the substrate).
What's NOT in this release (deferred)
- Catalog architectural redesign (behavioral fingerprinting, catalog-as-optimization) — deferred to alpha.24
onValidationRetryRegistry-level emission — deferred from alpha.21 planning- Streamed cost surfacing — deferred from alpha.21 planning
- LP-REQ-01 resilient fallback preset — deferred from alpha.20.1 planning
v0.1.0-alpha.22 — Reasoning-model architecture + adapter-google httpOptions (additive)
What's in this release
Two coordinated themes, all additive, no breaking changes.
Reasoning-model architecture cleanup (adapter-openai)
Driven by ADW's 2026-06-19 instrumentation of the multi-team agentic build loop. Empirical evidence: DeepInfra-hosted `openai/gpt-oss-120b` was silently failing as a reasoning model because the catalog regex `/^gpt-oss-/i` (anchored at `^`) couldn't match the provider-namespaced ID. Same model, same weights, different catalog answer based purely on which provider was hosting it. The fix isn't another regex — that's catalog-debt-by-another-name. The fix is architectural.
1. Model-ID normalization. New `normalizeModelId` helper strips a `/` namespace prefix, returning the canonical name (substring after the last `/`). Every adapter-side call to the capability learner normalizes first. Catalog patterns stay anchored at `^`; canonical names match regardless of which provider serves the model.
- `gpt-oss-120b` → `gpt-oss-120b` (OpenAI-native; unchanged)
- `openai/gpt-oss-120b` → `gpt-oss-120b` (DeepInfra/Groq form)
- `deepseek-ai/DeepSeek-V4-Flash` → `DeepSeek-V4-Flash`
- `XiaomiMiMo/MiMo-V2.5` → `MiMo-V2.5` (Parasail)
- `google/gemma-4-31B-it` → `gemma-4-31B-it`
The raw model ID is still used in SDK request bodies. Normalization is scoped to the capability-learning layer only.
Architectural payoff: the same canonical model served by two providers (Cerebras `gpt-oss-120b` and DeepInfra `openai/gpt-oss-120b`) shares learned state. A constraint learned at runtime for one is visible to the other.
2. Broadened runtime reasoning detection. Three changes to the runtime detection path so it catches provider-specific response shapes the previous narrow assumptions missed:
- `learnFromResponse` now reads `message.reasoning_content` (DeepInfra's harmony field) in addition to `message.reasoning` (Cerebras) and `usage.completion_tokens_details.reasoning_tokens` (OpenAI native).
- `reasoningStarvedResponse` accepts `finish_reason: "stop"` in addition to `"length"`. DeepInfra's gpt-oss harmony serving returns `stop` despite the model not having finished.
- `reasoningStarvedResponse` also checks `message.tool_calls`: if the model emitted executable tool calls, the response is NOT starved (regression guard against rescuing successful tool-use).
3. Xiaomi MiMo catalog entry. `MiMo-V\d+` family added to `KNOWN_REASONING_MODELS` (distinct from MiniMax despite the name similarity). Filed 2026-06-19 after observing mimo-parasail starvation in ADW production.
adapter-google `httpOptions` pass-through
`createGoogleAdapter` now forwards `opts.httpOptions` verbatim to the underlying `@google/genai` `GoogleGenAI` constructor. The most common use case: a backend proxy that holds the real `GEMINI_API_KEY` and exposes a Bearer-token-authenticated endpoint to a browser bundle.
```ts
const adapter = createGoogleAdapter({
apiKey: process.env.YOUR_BACKEND_BEARER!, // Bearer for YOUR backend
httpOptions: {
baseUrl: "https://your-app.example/api/llm/google\",
// also: apiVersion, headers, timeout, retryOptions
},
});
```
`HttpOptions` re-exported from `@llm-ports/adapter-google` so consumers type their override without an extra peer dep.
What's still NOT fixed
DeepInfra-served gpt-oss tool-use still doesn't execute the model's tool-call intent. With alpha.22 the budget is correct (multiplier applies on call 1), the starvation rescue fires (giving the model a second chance), but the tool-call intent often lands in `message.reasoning_content` rather than `message.tool_calls`. Until the adapter parses the harmony channel for tool calls — a separate research-first workstream — `runAgent` against DeepInfra-served gpt-oss may still not execute the model's intended tool calls. For tool-use workloads against gpt-oss, route to Cerebras.
Backwards compatibility
All changes are additive. Existing callers, existing tests, existing routing — all unchanged. Adapter-openai gets stricter recognition of reasoning models (the pre-alpha.22 false negatives become positives); adapter-google gets a new optional field.
Tests
- 15 new model-ID normalization tests (canonical-name extraction, catalog matching across all known provider prefixes, shared learner state, user-supplied capability precedence)
- 7 new reasoning detection broadened tests (DeepInfra harmony shape recognition, finish=stop rescue, OpenAI o-series regression, 4 no-spurious-rescue regression guards)
- 5 new `httpOptions` pass-through tests (baseUrl forwarding, full options forwarding, no-op when omitted, no-op when undefined, type reachable)
- 2 MiMo catalog tests (matches family, distinct from MiniMax, doesn't false-positive on Mimosa-V*)
Total: 763 unit tests pass (was 736; +29, 0 regressions across the other 6 packages).
Empirical sources
- ADW `Development_Logs.md` commit b1eeee2 — code-grounded root cause of the deepseek + gpt-oss DeepInfra tool-loop failures
- Raw 2-turn DeepInfra probe (2026-06-19): `finish: "stop"`, `content: ""`, `tool_calls: []`, `reasoning_content: '{"path": "", "depth": 3}\n'`
- ADW production incident 2026-06-19T15:40 UTC: mimo-parasail (XiaomiMiMo/MiMo-V2.5) starved on a structured-output decompose call
Install
`pnpm add @llm-ports/adapter-openai@alpha @llm-ports/adapter-google@alpha`
`@llm-ports/core`, `@llm-ports/capabilities`, and the other adapters stay at `0.1.0-alpha.21` (no changes; @Alpha tag still resolves correctly).
What's NOT in this release (deferred)
- `onValidationRetry` Registry-level emission (still type-only; deferred from alpha.21 planning)
- Streamed cost surfacing for `onCost` / `onTokenUsage` (deferred from alpha.21 planning)
- LP-REQ-01 resilient fallback preset (deferred from alpha.20.1 planning)
- Harmony-channel tool-call parser (research-first; pending more empirical evidence)
These will land in alpha.23 or later. Scope discipline: alpha.21 fixed structured-output reliability; alpha.22 fixes reasoning-model handling. Each release stays themed.
v0.1.0-alpha.21 — Structured-output reliability + OTel observability hooks (additive)
What's in this release
Two coordinated themes, all additive, no breaking changes.
Structured-output reliability (closes #46, #47, #48)
Three changes driven by ADW's 2026-06-18 LLM Provider Test Report showing that DeepInfra + Parasail were the only cheap-tier providers dropping required fields on the first structured-output attempt:
- Per-call
strict?: booleanonGenerateStructuredOptionsandStreamStructuredOptions. Precedence: per-call > adapter-level > auto-detect. Lets registries with one adapter alias per provider flip strict on/off per call based on the schema shape (closed-shape → strict,z.record(...)→ json_object). Plumbed through the 5 structured-output capability factories (createClassifier/createScorer/createExtractor/createAnalyzer/createPlanner). - Strict-mode allowlist in
autoDetectStrictResponseFormatextended toapi.deepinfra.comandapi.parasail.io. ADW's 8-calls-per-provider sweep: deepseek-flash 2/8 → 0/8 retries, gemma-31b 8/8 → 0/8, mimo-parasail 3/8 → 0/8. - Bundled pricing entries for three compat models in active production use against the verified-OK matrix:
deepseek-ai/DeepSeek-V4-Flash($0.10 / $0.20 per 1M, DeepInfra),google/gemma-4-31B-it($0.10 / $0.20 per 1M, DeepInfra),XiaomiMiMo/MiMo-V2.5($0.14 / $0.28 per 1M, Parasail). Consumers no longer need a parallelpricingOverridestable for these.
The isJsonModeRejection matcher fix from #46 section 1 was already in main as 9cb9a82 and is now bundled in alpha.21 as well.
OTel-aligned observability hooks
RegistryOptions.observability accepts a bundle of five fire-and-forget hooks aligned with OpenTelemetry's `gen_ai.*` semantic-conventions taxonomy so downstream pipelines (Honeycomb, Datadog, OTel Collector, custom OTLP exporters) can map them onto spans + metrics without re-deriving fields:
```ts
const registry = createRegistryFromEnv({
// ...existing options...
observability: {
onCost: (e) => { /* per-call USD breakdown / },
onTokenUsage: (e) => { / per-call token counts / },
onFallback: (e) => { / fired when chain advances / },
onCacheHit: (e) => { / fired when cached_tokens > 0 / },
onValidationRetry: (e) => { / type only in alpha.21 */ },
},
});
```
All hooks sync OR async, errors swallowed (observability can't break inference).
Emission coverage
| Hook | Emitted in alpha.21? | Where |
|---|---|---|
| `onCost` | ✅ | Every successful `generateText` / `generateStructured` / `runAgent` |
| `onTokenUsage` | ✅ | Every successful `generateText` / `generateStructured` / `runAgent` |
| `onCacheHit` | ✅ | When the response reports `cacheReadTokens > 0` |
| `onFallback` | ✅ | When `walkChain` advances from one alias to the next |
| `onValidationRetry` | Type only (emission deferred to alpha.22) | Use adapter `onRetry` with `reason === "validation-feedback"` today |
Stream methods do not emit cost yet (streamed cost surfacing is the alpha.22 follow-up).
Backwards compatibility
All four additions are additive. Calls without `strict`, registries without `observability`, adapters against allowlisted baseURLs unchanged. Bundled pricing additions are pure-additive.
Tests
- 14 new adapter-openai tests (per-call strict precedence + DeepInfra/Parasail allowlist + bundled pricing)
- 22 new core observability-hooks tests (4 emitted hooks + 3 emit helpers + deriveCacheHit pure helper + Registry backwards-compat regression)
- All existing 505 unit tests across core + capabilities + 5 adapters pass
What's next (alpha.22 plan)
- `onValidationRetry` Registry-level emission
- Streamed cost surfacing for `onCost` / `onTokenUsage`
- Optional Cerebras pricing entries pending rate confirmation
- LP-REQ-01 resilient fallback preset (carried over from alpha.20.1)
Install: `pnpm add @llm-ports/core@alpha @llm-ports/adapter-openai@alpha @llm-ports/capabilities@alpha`
0.1.0-alpha.20.1 — Migration safeguards (codemod + postinstall banner + MIGRATION.md + per-release docs)
TL;DR. No new public surface. This release adds the safety rails that should have shipped alongside the breaking changes in alpha.18, alpha.19, and alpha.20. Seven coordinated mechanisms (A-G below) make it harder for production code to silently upgrade across a breaking alpha. Plus the migration page for alpha.19 → alpha.20 that the alpha.20 release should have shipped.
Install: npm install @llm-ports/core@alpha resolves to 0.1.0-alpha.20.1 now.
Backwards compatible with alpha.20 in every other respect. The next planned release is alpha.21 on 2026-06-20: OTel-aligned observability hooks.
What landed
A. Migration page: alpha.19 → alpha.20
New file: docs/migration/alpha-19-to-alpha-20.md. Documents the one TypeScript-level breaking change that alpha.20 contained and the alpha.20 release notes glossed over: BudgetLimit.requestsPerHour went from required to optional. One-line fix or codemod (see C below).
Added to the VitePress sidebar so users browsing the docs site can find it without grepping CHANGELOGs.
B. Postinstall banner in @llm-ports/core
When @llm-ports/core changes versions between installs, a one-line banner prints to stdout pointing at MIGRATION.md:
ⓘ @llm-ports/core upgraded 0.1.0-alpha.20 → v0.1.0-alpha.20.1
migration: https://github.com/baabakk/llm-ports/blob/main/MIGRATION.md
disable banner: export LLM_PORTS_NO_NOTICE=1
Constraints (deliberately conservative for a postinstall script):
- Skips on CI (
CI=trueorCONTINUOUS_INTEGRATION=true). - Skips when stdout is not a TTY (redirected output, log capture, etc.).
- Skips when
LLM_PORTS_NO_NOTICE=1is set. - Bails silently on any error. A failing postinstall that blocks
npm installis worse than a missing banner. - Marker file lives inside the package install dir; reinstalls of the same version stay quiet.
- No network calls, no outside-tree writes.
C. New @llm-ports/migrate codemod package
New scoped package. CLI binary llm-ports-migrate plus programmatic API. Ships one codemod for now: alpha-19-to-alpha-20, which rewrites the requestsPerHour reads:
# Preview (dry-run is the default):
npx @llm-ports/migrate@alpha alpha-19-to-alpha-20
# Apply:
npx @llm-ports/migrate@alpha alpha-19-to-alpha-20 --write
# Custom path:
npx @llm-ports/migrate@alpha alpha-19-to-alpha-20 ./apps/web --writeDiff produced by the codemod:
- const rph = entry.budgetLimit.requestsPerHour;
+ const rph = (entry.budgetLimit.requestsPerHour ?? Infinity);Conservative by design. The codemod skips matches already followed by ? (optional chaining or nullish coalescing already present), flags matches inside if ( conditions as manual-review, flags assignment LHS as manual-review. 9 tests in packages/migrate/tests/alpha-19-to-alpha-20.test.ts cover every case.
D. Exact-version pinning convention
README.md, docs/getting-started.md, and docs/v0-1-status.md now recommend exact-version pins during the alpha line:
// package.json — recommended during alphas
{
"dependencies": {
"@llm-ports/core": "0.1.0-alpha.20.1"
}
}Why: the @alpha dist-tag tracks the latest published prerelease. A pnpm install or npm update can therefore jump you across breaking changes silently. An exact pin locks you to a known-good version until you deliberately bump. The @alpha tag is documented as "fine for experimentation" but not for production deps.
E. Top-level MIGRATION.md
New file at the repo root. TL;DR table indexes every release with its migration impact and links to per-release pages. Single source of truth that both humans and automation tools can scan.
F. GitHub "breaking" label + release-title convention
The breaking label (color #B60205, description points at MIGRATION.md) is the label for issues and PRs that propose breaking changes. For releases (which don't have GitHub labels), the convention is to tag the release title — alpha.18 was (BREAKING), alpha.19 was (BREAKING), and alpha.20's title is now retroactively (TS-BREAKING: requestsPerHour now optional) since we now know about the type-level break.
G. @llm-ports/consumer-type-check smoke package
New private workspace package (not published) that imports the public surface of every @llm-ports/* package and uses it in strict TypeScript mode with every paranoid flag enabled (strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes, noUnusedLocals, noUnusedParameters). Compiles via the existing recursive pnpm typecheck job in CI.
This is the mechanism that would have caught the alpha.20 requestsPerHour break at the source. Per-package tests only see internals; a consumer-shaped strict typecheck sees what real users see.
Test stats
687 passing across 8 published packages (was 678 in alpha.20; +9 new from the migrate codemod). Plus 21 packages typechecking cleanly under pnpm -r typecheck (was 19; +2 from the new packages).
What did not change
- No new public API on
@llm-ports/coreor any adapter. - No runtime behavior changes vs alpha.20.
- The
BudgetScope+ minute / session gating from alpha.20 is unchanged. - No new dependencies.
- The
@llm-ports/migratepackage has zero runtime dependencies.
Where this fits
This release is the close-out of a discipline lesson, not a new feature drop. The alpha.18 → alpha.19 → alpha.19.1 → alpha.20 sequence shipped three breaking changes (typed errors, the cache field rename, and the requestsPerHour type tweak). Without MIGRATION.md, the codemod, the postinstall banner, and the pinning convention, a user on @alpha could have been broken three times in two weeks with nothing pointing at the fix. Alpha.20.1 retroactively builds the rails.
The next planned release stays on schedule: alpha.21 on 2026-06-20 with OTel-aligned observability hooks. Then beta.0 on 2026-06-30 with the surface locked. Beta minors are additive only.
0.1.0-alpha.20 — BudgetScope + minute/session gating grammar (TS-BREAKING: requestsPerHour now optional)
TL;DR. Per-tenant cost gating, minute-grain rate limits, per-session token / tool-call ceilings. Five-tier scope hierarchy. All shipped with end-to-end runtime tests, not just types. Backwards compatible — existing callers and env configs see identical behavior.
Install: npm install @llm-ports/core@alpha resolves to 0.1.0-alpha.20 now.
This is the fourth of four shape-locks before beta.0 on 2026-06-30. One more alpha lands Friday: alpha.21 with observability hook signatures aligned with OpenTelemetry gen_ai.* semantic conventions. Then the surface stops moving.
The two big things
1. Per-scope gating
type BudgetScope = "tenant" | "customer" | "user" | "agent" | "session";
interface BudgetScopeRef {
scope: BudgetScope;
scopeId: string;
}budgetScope?: BudgetScopeRef is now an optional field on every request option type (chat, structured, streaming, agent, embeddings). When set, the Registry composes the gating storage key as ${alias}|${scope}:${scopeId} so every configured cap applies per-tenant / per-customer / per-user / per-agent / per-session instead of per-alias. Omit it and you get alpha.19.1 per-alias behavior unchanged.
// Same provider config, two tenants. Each tenant gets its own $50/day budget.
await llm.generateText({
taskType: "triage",
prompt: messageForAcme,
budgetScope: { scope: "tenant", scopeId: "acme" },
});
await llm.generateText({
taskType: "triage",
prompt: messageForInitech,
budgetScope: { scope: "tenant", scopeId: "initech" },
});The noisy customer no longer burns the rest of your budget. The .env stays identical — cost:50/day now means "per scope" rather than "global to this alias".
2. Five new env gating tokens
parseGating accepts these in addition to the alpha.19 set:
| Token | Meaning | Enforced by |
|---|---|---|
req:N/minute |
Request rate per minute | InMemoryBudget |
cost:N/minute |
USD cap per minute | InMemoryCost |
cost:N/session |
USD cap per CostSession |
CostSession.budgetUSD |
req:N/session |
Request cap per CostSession |
CostSession.maxRequests |
total_tokens:N/session |
Total tokens (input + output) per session | CostSession.maxTokens |
tool_calls:N/session |
Tool / function calls per session | CostSession.maxToolCalls |
The minute window finally expresses Cerebras 30 RPM correctly. Before alpha.20, req:30/hour would let through 1800 calls in a single noisy second; req:30/minute enforces the actual provider cap.
The session-grain tokens land on CostSession. When one trips, SessionBudgetExceededError now carries an optional grain field naming which cap fired:
try {
await session.getPort().runAgent({ ... });
} catch (err) {
if (err instanceof SessionBudgetExceededError) {
console.log(err.grain); // "tokens (50000 >= 50000)" or "tool_calls (8 >= 8)"
// or "requests (100 >= 100)" or undefined for USD cap
}
}Plus three new getters on CostSession: requestsMade(), tokensUsed(), toolCallsMade(). Useful for per-session attribution dashboards.
What's verified end-to-end
After the alpha.19/alpha.19.1 lesson, every cell of the behavior matrix has a test that proves runtime behavior, not just type acceptance. The 24 new tests in packages/core/tests/budget-scope.test.ts cover:
- All 7 new
parseGatingtokens with positive + negative cases. - All 5 BudgetScope axes (
tenant/customer/user/agent/session) — independent counters perscopeId, verified end-to-end through the Registry. - Cerebras 30 RPM minute-grain expressibility (closes the long-standing gap in
req:N/hour). - Per-tenant cost cap with two-tenant isolation against
InMemoryCost. - All 4 session-grain tokens enforced via
CostSession.
Total test stats: 678 passing across 7 packages (was 654 in alpha.19.1; +24 new).
Backwards compatibility
req:N/hourstill works exactly as it did in alpha.19. The legacyBudgetLimit.requestsPerHourfield is still populated so any backend that hasn't been upgraded to readperHourkeeps working.- Existing callers who omit
budgetScopesee identical behavior to alpha.19.1. - No adapter changes. No capability changes.
- Adapter conformance suite is unchanged.
SessionBudgetExceededError's alpha.18 constructor signature is unchanged. Thegrainfield is optional and only populated for the new ceilings.
What's next
| Release | Date | Surface |
|---|---|---|
| alpha.17 | 2026-06-05 | RerankPort skeleton, BackoffConfig, onRetry parity |
| alpha.18 | 2026-06-05 | Typed error taxonomy |
| alpha.19 | 2026-06-12 | CacheControl shape + cacheSavingsUSD rename |
| alpha.19.1 | 2026-06-12 | CacheControl behavior across cloud adapters + capability pass-through |
| alpha.20 | 2026-06-13 | BudgetScope + minute / session gating grammar |
| alpha.21 | 2026-06-20 (target) | OTel-aligned observability hooks (onCost, onTokenUsage, onFallback, onValidationRetry, onCacheHit) |
| beta.0 | 2026-06-30 (target) | Scope-closed; surface stops moving |
Then the beta minors start delivering — resilient fallback preset, adapter-anthropic extended-thinking fixes, persistent Redis-backed budget backend, pluggable CacheBackend, capability factories, RerankPort's first adapter. See the master plan for the full schedule.
Found a real gap before beta.0? File an issue. The shape locks but additive fields are still on the table.
0.1.0-alpha.19.1 — CacheControl behavior across cloud adapters + capability factories
TL;DR. alpha.19 locked the CacheControl shape; alpha.19.1 backs that shape with verified per-mode behavior on the cloud adapters and pass-through on every capability factory. The promise of alpha.19 now actually holds at runtime.
Install: npm install @llm-ports/core@alpha resolves to 0.1.0-alpha.19.1 now.
No breaking changes vs alpha.19. The four-alpha sequence to beta.0 on 2026-06-30 is unchanged: alpha.20 (BudgetScope, 2026-06-17) and alpha.21 (observability hooks, 2026-06-20) still lead beta.0.
Why this exists
The alpha.19 release notes used present-tense claims like "Anthropic places a cache_control marker at the last static block" — but the alpha.19 commit only landed the type and the field rename. Adapters and capabilities accepted the field without acting on it. The docs were aspirational; the runtime was a no-op. Babak flagged it: "have you made cache enabled on all the capabilities and providers by default?" The honest answer was no.
alpha.19.1 makes the answer yes.
What changed
@llm-ports/adapter-anthropic
Translates CacheControl into Anthropic's cache_control: { type: "ephemeral", ttl? } markers across all five SDK call sites (generateText, generateStructured, streamText, streamStructured, runAgent).
mode: "auto"withinstructionsset → promotessystem: stringto the structuredsystem: [{ type: "text", text, cache_control: { type: "ephemeral", ttl? } }]shape. Noinstructions→ no-op.mode: "manual"honorsbreakpoints[]:{ at: "system" }→ marker on system block.{ at: "tools" }→ marker on last tool entry.{ at: "message-index", index }→ marker on last content block ofmessages[index](promoting string content to a structured array when needed).- Empty breakpoints array → falls back to system placement.
mode: "off"→ explicit no-op (matches the natural default; intent: caller is opting out for this call only).mode: "preCreated"→ no-op (Anthropic has nocreateCachedContenthandle pattern).ttlSeconds: 3600→ emitsttl: "1h".ttlSeconds: 300or undefined → omitsttl(Anthropic default 5m). Other values omitttlper Anthropic's enum.
10 new tests in packages/adapter-anthropic/tests/quirks/cache-control.test.ts cover every combination above.
@llm-ports/adapter-google
Wires mode: "preCreated" to Gemini's config.cachedContent field on every generateContent / generateContentStream call.
mode: "preCreated"withcachedContentHandle→ setsconfig.cachedContent = cachedContentHandle.mode: "preCreated"without a handle / empty handle → no-op.mode: "auto" | "manual" | "off"→ no-op (no caller-controllable Gemini surface). The adapter intentionally does nothing rather than silently substituting a different mechanism.
7 new tests in packages/adapter-google/tests/quirks/cache-control.test.ts.
The cached-content lifecycle helper that wraps cachedContents.create() ships in @llm-ports/capabilities in beta.2. Until then callers create the handle themselves and pass it via cacheControl.
@llm-ports/capabilities
All 7 factories thread cacheControl from their per-call input to the underlying port call:
createClassifier,createScorer,createExtractor,createAnalyzer,createPlanner→port.generateStructured({ ..., cacheControl }).createDrafter,createSummarizer→port.generateText({ ..., cacheControl }).
CapabilityEvent.cost gains cacheSavingsUSD?. When the underlying adapter returns cache telemetry, the savings number propagates through the factory's onResult event so consumers can attribute per-capability savings on their dashboards.
11 new tests in packages/capabilities/tests/cache-control-passthrough.test.ts.
OpenAI / Ollama / Vercel
No behavior change. Documented as deliberate no-ops:
- OpenAI: implicit prompt cache is always on, no API to influence it. OpenAI-compat providers (Cerebras, Groq, Fireworks, Together AI, SambaNova, etc.) inherit the same documented no-op.
- Ollama: local models, no billed prompt cache surface.
- Vercel bridge: caching is configured at the bridged provider, not at this layer.
The field is accepted on every adapter so forward-compatible callers don't need a per-provider switch. When alpha.21 lands observability hooks (next Friday), the onCacheHit hook fires uniformly regardless of which adapter is underneath.
docs/concepts/cache.md
Rewritten as a verified behavior matrix. Each cell points at the test file that proves it. Worked examples for Anthropic auto and Gemini preCreated. The aspirational present-tense claims from alpha.19 are gone.
Test stats
654 passing across 7 packages (was 626 in alpha.19; +28 new from +10 anthropic + +7 google + +11 capability).
Versioning note
This is 0.1.0-alpha.19.1, not alpha.20. Manual bump (changeset pre-mode would have rolled to alpha.20 by default) so the planned alpha.19 → alpha.20 → alpha.21 → beta.0 sequence stays intact:
| Release | Date | Surface |
|---|---|---|
| alpha.17 | 2026-06-05 | RerankPort skeleton, BackoffConfig, onRetry parity |
| alpha.18 | 2026-06-05 | Typed error taxonomy |
| alpha.19 | 2026-06-12 | CacheControl shape + cacheSavingsUSD rename |
| alpha.19.1 | 2026-06-12 | CacheControl behavior across cloud adapters + capability pass-through |
| alpha.20 | 2026-06-17 (target) | BudgetScope 5-tier + minute / session gating |
| alpha.21 | 2026-06-20 (target) | OTel-aligned observability hook signatures |
| beta.0 | 2026-06-30 (target) | Scope-closed; surface stops moving |
What did not change
- No new public types. The
CacheControlshape, thecacheSavingsUSDfield name, and the request option types are byte-identical to alpha.19. - No adapter factory signatures changed. No environment variable names changed.
- Existing callers who omit
cacheControlsee identical behavior to alpha.19. - Adapter conformance test suite is unchanged.
If alpha.19 worked for you, alpha.19.1 works for you — except now the field actually does what the docs say it does.