LLMClient.prepare(request) returned a PreparedRequest with target: unknown.
Callers building debug UIs / request previews / plan rendering had to cast
target to the adapter's native shape at every read.
Adds PreparedRequestOf<Target> in schema and a generic Target = unknown
parameter on LLMClient.prepare so callers can opt in to a typed view:
const prepared = yield* client.prepare<OpenAIChatTarget>(request)
prepared.target.model // typed
prepared.target.messages // typed
The runtime payload is unchanged — the adapter still emits target: unknown
and the consumer asserts the shape they expect from the configured adapter.
The cast lives at the public boundary in adapter.ts; everything else stays
honest about runtime types.
Existing callers without the type argument still get target: unknown and
nothing breaks. Test in openai-chat.test.ts proves the narrowing at the
type level.
Schema.toTaggedUnion('type') already provides LLMEvent.guards but uses
kebab-case bracket access (LLMEvent.guards['tool-call']). Adds an LLMEvent.is
namespace with camelCase aliases that delegate to the same guards, so
consumers can write events.filter(LLMEvent.is.toolCall) instead of
events.filter(LLMEvent.guards['tool-call']).
Migrated all callsites in src/llm.ts and the two test files for consistency.
LLMEvent.guards / .match / .cases / .isAnyOf remain available for callers
who want the Effect-canonical API.
After the auth-axis migration, the OpenCode bridge consults this enum
solely to decide whether to read `provider.key` and stamp it on
`model.apiKey`. The bearer / anthropic-api-key / google-api-key
distinctions used to control which header the bridge wrote; that is now
the adapter's Auth axis's job.
Three of four variants were write-only after the migration. Collapse to:
- 'key' — provider needs an API key
- 'none' — provider does not (e.g. local)
Updated all six provider resolvers and the resolver test fixtures.
Two cleanups to make the adapter constructor surface honest about what is
canonical and what is an escape hatch:
- Adapter.compose existed to override pieces of an existing adapter, used
by OpenAI-compatible Chat before the four-axis migration. After the
migration nothing references it; OpenAI-compatible Chat composes via
fromProtocol({ protocol: OpenAIChat.protocol, ... }) instead. Delete
the function and its ComposeInput type.
- Adapter.define is the lower-level escape hatch for adapters whose
behavior genuinely cannot fit the Protocol/Endpoint/Auth/Framing model.
Its name implied it was the canonical entry point. Renamed to
Adapter.unsafe so the four-axis Adapter.fromProtocol(...) reads as the
obvious primary path and the escape hatch carries its escape semantics
in its name.
Updated test fixtures in adapter.test.ts and the AGENTS.md guidance.
The optional 'provider' field on Adapter / AdapterInput / FromProtocolInput
existed as a registry filter: requests with a different model.provider could
not find adapters that set it. After the four-axis migration no adapter
needed it (and an earlier pass removed it from the five migrated providers
because setting it broke session/llm-native tests).
Drop the field entirely and collapse the registry to a single-tier protocol
lookup. If a future deployment genuinely needs to be scoped (e.g. an
Azure-only OpenAI Responses adapter), reintroduce as 'scopedTo' with an
explicit name. Solve when needed, not before.
Also drops the test that exercised the now-removed two-tier lookup
('prefers provider-specific adapters over protocol fallbacks').
The commit that promoted queryParams to a typed ModelRef field updated
the implementation but left two JSDoc/doc references pointing at the old
model.native.queryParams path.
Promotes queryParams to a first-class ModelRef field used by Endpoint.baseURL,
so deployment-level URL query params (Azure api-version, OpenAI-compatible
provider knobs) live in a typed home instead of an opaque `native` bag.
Also removes write-only dead fields from `native`:
- openaiCompatibleProvider (set by family helper, never read)
- opencodeProviderID, opencodeModelID (set by opencode bridge + native session
builder, never read)
- npm (set by opencode bridge, never read)
After this commit `model.native` only carries genuinely provider-specific
opaque options that no other adapter cares about (Bedrock's aws_credentials
+ aws_region for SigV4). Drops the now-dead ProviderShared.queryParams
helper. Updates AGENTS.md doc on native is implicit through the new schema
JSDoc.
The two paths are independent: `model.apiKey` produces a synchronous
Bearer auth, while AWS credentials need an effectful sigv4 sign.
Hoist the bearer path out of `Effect.gen` and reuse `Auth.bearer`
directly, keeping the SigV4 path as a focused `Effect.gen` that owns
the credential lookup, signing, and header merge.
Inlines the now single-use `headersForSigning` and `signed` setup.
After the apiKey migration, every adapter explicitly specified `auth`,
and three of them (OpenAI Chat, OpenAI Responses, OpenAI-compatible Chat)
all wrote `auth: Auth.bearer`. `Auth.bearer` is a no-op when
`model.apiKey` is unset, so making it the default is strictly safer than
the previous `Auth.passthrough` default — bearer-style adapters drop
their explicit `auth` line, and adapters that need a different scheme
opt out via `Auth.apiKeyHeader(...)` (Anthropic, Gemini) or a custom
`Auth` (Bedrock SigV4 + Bearer).
Update doc comments on `fromProtocol.auth`, `Auth` type, and
`packages/llm/AGENTS.md` to reflect the new default.
Add an optional `apiKey` field to `ModelRef` so authentication is no
longer baked into `model.headers` at construction time. Each provider
adapter now passes an `Auth` to `Adapter.fromProtocol` that reads
`request.model.apiKey` per request:
- OpenAI Chat / Responses / OpenAI-compatible Chat: `Auth.bearer`
- Anthropic Messages: `Auth.apiKeyHeader("x-api-key")`
- Gemini: `Auth.apiKeyHeader("x-goog-api-key")`
- Bedrock Converse: custom auth that uses `apiKey` for Bearer auth
and falls back to SigV4 with AWS credentials
The `model()` constructors no longer fold the API key into
`model.headers`. The OpenCode bridge sets `apiKey` directly instead of
building auth headers via the now-deleted `authHeader` helper. Test
assertions move from `headers: { authorization: "Bearer ..." }` to
`apiKey: "..."`.
Both helpers had the same shape: read `request.model.apiKey`, no-op if
absent, otherwise merge a one-key header object. Lift that into a tiny
`fromApiKey(from)` helper and define both in terms of it.
The public surface (`Auth.bearer`, `Auth.apiKeyHeader`) is unchanged.
Removes the provider field from the five migrated Adapter.fromProtocol
calls. Setting provider scopes the adapter in the registry so requests
must use the same provider id, which broke session/llm-native tests
that build models with provider 'amazon-bedrock' against the
bedrock-converse adapter.
Adapters should stay protocol-only by default and only set provider
when the deployment is genuinely scoped (e.g. an Azure-only adapter
that does not work for native OpenAI). Restoring the original
protocol-only registration.
Per style guide, single-use values should be inlined. Each adapter had
a module-private constant used exactly once in its Adapter.fromProtocol
call. Inlining removes 5 named constants (4 DEFAULT_BASE_URL + 1
defaultBaseURL + ANTHROPIC_VERSION) without loss of clarity — the
string literal appears at the point of use.
After migration to Adapter.fromProtocol, the sse() convenience wrapper
and withQuery() URL builder are no longer called anywhere — Framing.sse
and Endpoint.baseURL handle their responsibilities directly. Also
inlines two exported-but-unused test constants (helloPrompt,
weatherPrompt) per style guide.
Updates the AGENTS.md adapter section to describe the four orthogonal
axes that make up an adapter today (Protocol + Endpoint + Auth + Framing)
and the canonical Adapter.fromProtocol composition. Adds a folder layout
overview so the dependency direction (provider/* imports protocol/auth/
endpoint/framing, never the other way) is visible.
Extracts a Protocol implementation per provider and wires the adapter
through Adapter.fromProtocol with explicit Endpoint, Auth, and Framing:
- OpenAI Responses — Endpoint.baseURL with /responses path.
- Anthropic Messages — adds anthropic-version header via the headers slot.
- Gemini — endpoint embeds the model id and pins ?alt=sse at the URL level.
- Bedrock Converse — keeps SigV4-or-Bearer auth as a typed Auth function;
AWS event-stream framing is a typed Framing value alongside the protocol;
Endpoint.baseURL gains a function-typed default so the URL host can carry
the per-request region.
Recorded replay byte-identical across all six adapters; full provider
suite 83 pass, full llm suite 122 pass, opencode typecheck clean.