simplify(llm): default Adapter.fromProtocol auth to Auth.bearer

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.
This commit is contained in:
Kit Langton
2026-04-28 18:31:06 -04:00
parent 5d08e28cd9
commit 042bf6c822
6 changed files with 15 additions and 15 deletions
+2 -2
View File
@@ -41,7 +41,7 @@ An adapter is the registered, runnable composition of four orthogonal pieces:
- **`Protocol`** (`src/protocol.ts`) — semantic API contract. Owns request lowering, target validation, body encoding, and the streaming chunk-to-event state machine. Examples: `OpenAIChat.protocol`, `OpenAIResponses.protocol`, `AnthropicMessages.protocol`, `Gemini.protocol`, `BedrockConverse.protocol`.
- **`Endpoint`** (`src/endpoint.ts`) — URL construction. Receives the request and the validated target so it can read `model.id`, `model.baseURL`, `model.native.queryParams`, and any target field that influences the URL (e.g. Bedrock's `modelId` segment). Reach for `Endpoint.baseURL({ default, path })` before hand-rolling a URL.
- **`Auth`** (`src/auth.ts`) — per-request transport authentication. Most adapters use `Auth.passthrough`: their auth header is statically baked into `model.headers` by their `model()` constructor. Adapters that need per-request signing (Bedrock SigV4, future Vertex IAM, Azure AAD) implement `Auth` as a function that signs the body and merges signed headers into the result.
- **`Auth`** (`src/auth.ts`) — per-request transport authentication. Adapters read `model.apiKey` at request time via `Auth.bearer` (the `Adapter.fromProtocol` default; sets `Authorization: Bearer <apiKey>`) or `Auth.apiKeyHeader(name)` for providers that use a custom header (Anthropic `x-api-key`, Gemini `x-goog-api-key`). Adapters that need per-request signing (Bedrock SigV4, future Vertex IAM, Azure AAD) implement `Auth` as a function that signs the body and merges signed headers into the result.
- **`Framing`** (`src/framing.ts`) — bytes → frames. SSE (`Framing.sse`) is shared; Bedrock keeps its AWS event-stream framing as a typed `Framing<object>` value alongside its protocol.
Compose them via `Adapter.fromProtocol(...)`:
@@ -74,7 +74,7 @@ packages/llm/src/
protocol.ts // Protocol type + Protocol.define
endpoint.ts // Endpoint type + Endpoint.baseURL
auth.ts // Auth type + Auth.passthrough
auth.ts // Auth type + Auth.bearer / Auth.apiKeyHeader / Auth.passthrough
framing.ts // Framing type + Framing.sse
provider/
+9 -3
View File
@@ -1,7 +1,7 @@
import { Effect, Stream } from "effect"
import { HttpClientRequest, type HttpClientResponse } from "effect/unstable/http"
import type { Auth } from "./auth"
import { passthrough as authPassthrough } from "./auth"
import { bearer as authBearer } from "./auth"
import type { Endpoint } from "./endpoint"
import * as LLM from "./llm"
import { RequestExecutor } from "./executor"
@@ -143,7 +143,13 @@ export interface FromProtocolInput<Draft, Target, Frame, Chunk, State> {
readonly protocol: Protocol<Draft, Target, Frame, Chunk, State>
/** Where the request is sent. */
readonly endpoint: Endpoint<Target>
/** Per-request transport authentication. Defaults to `Auth.passthrough`. */
/**
* Per-request transport authentication. Defaults to `Auth.bearer`, which
* sets `Authorization: Bearer <model.apiKey>` when `model.apiKey` is set
* and is a no-op otherwise. Override with `Auth.apiKeyHeader(name)` for
* providers that use a custom header (Anthropic, Gemini), or supply a
* custom `Auth` for per-request signing (Bedrock SigV4).
*/
readonly auth?: Auth
/** Stream framing — bytes -> frames before `protocol.decode`. */
readonly framing: Framing<Frame>
@@ -177,7 +183,7 @@ export interface FromProtocolInput<Draft, Target, Frame, Chunk, State> {
export function fromProtocol<Draft, Target, Frame, Chunk, State>(
input: FromProtocolInput<Draft, Target, Frame, Chunk, State>,
): AdapterDefinition<Draft, Target> {
const auth = input.auth ?? authPassthrough
const auth = input.auth ?? authBearer
const protocol = input.protocol
const buildHeaders = input.headers ?? (() => ({}))
+4 -4
View File
@@ -7,10 +7,10 @@ import type { LLMError, LLMRequest } from "./schema"
* Receives the unsigned HTTP request shape (URL, method, body, headers) and
* returns the headers to actually send.
*
* Most adapters use `Auth.passthrough`: their auth header
* (`Authorization: Bearer ...`, `x-api-key`, `x-goog-api-key`) is already
* baked into `model.headers` by the provider's `model()` constructor, and
* `Auth` has nothing to do per request.
* Most adapters use the default `Auth.bearer`, which reads
* `request.model.apiKey` and sets `Authorization: Bearer ...`. Providers
* that use a different header pick `Auth.apiKeyHeader(name)` (e.g.
* Anthropic's `x-api-key`, Gemini's `x-goog-api-key`).
*
* Adapters that need per-request signing (AWS SigV4, future Vertex IAM,
* future Azure AAD) implement `Auth` as a function that hashes the body,
-2
View File
@@ -1,6 +1,5 @@
import { Effect, Schema } from "effect"
import { Adapter } from "../adapter"
import { Auth } from "../auth"
import { Endpoint } from "../endpoint"
import { Framing } from "../framing"
import { capabilities, model as llmModel, type ModelInput } from "../llm"
@@ -356,7 +355,6 @@ export const adapter = Adapter.fromProtocol({
id: ADAPTER,
protocol,
endpoint: Endpoint.baseURL({ default: "https://api.openai.com/v1", path: "/chat/completions" }),
auth: Auth.bearer,
framing: Framing.sse,
})
@@ -1,5 +1,4 @@
import { Adapter } from "../adapter"
import { Auth } from "../auth"
import { Endpoint } from "../endpoint"
import { Framing } from "../framing"
import { capabilities, model as llmModel, type ModelInput } from "../llm"
@@ -36,7 +35,6 @@ export const adapter = Adapter.fromProtocol({
path: "/chat/completions",
required: "OpenAI-compatible Chat requires a baseURL",
}),
auth: Auth.bearer,
framing: Framing.sse,
})
@@ -1,6 +1,5 @@
import { Effect, Schema } from "effect"
import { Adapter } from "../adapter"
import { Auth } from "../auth"
import { Endpoint } from "../endpoint"
import { Framing } from "../framing"
import { capabilities, model as llmModel, type ModelInput } from "../llm"
@@ -386,7 +385,6 @@ export const adapter = Adapter.fromProtocol({
id: ADAPTER,
protocol,
endpoint: Endpoint.baseURL({ default: "https://api.openai.com/v1", path: "/responses" }),
auth: Auth.bearer,
framing: Framing.sse,
})