From 042bf6c822ee1cd9ba393a8836d2fd8c735faa83 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 28 Apr 2026 18:31:06 -0400 Subject: [PATCH] simplify(llm): default Adapter.fromProtocol auth to Auth.bearer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/llm/AGENTS.md | 4 ++-- packages/llm/src/adapter.ts | 12 +++++++++--- packages/llm/src/auth.ts | 8 ++++---- packages/llm/src/provider/openai-chat.ts | 2 -- packages/llm/src/provider/openai-compatible-chat.ts | 2 -- packages/llm/src/provider/openai-responses.ts | 2 -- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/llm/AGENTS.md b/packages/llm/AGENTS.md index a4c6202ee1..a248ee2ca2 100644 --- a/packages/llm/AGENTS.md +++ b/packages/llm/AGENTS.md @@ -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 `) 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` 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/ diff --git a/packages/llm/src/adapter.ts b/packages/llm/src/adapter.ts index 5dae25d61f..0448b16b41 100644 --- a/packages/llm/src/adapter.ts +++ b/packages/llm/src/adapter.ts @@ -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 { readonly protocol: Protocol /** Where the request is sent. */ readonly endpoint: Endpoint - /** Per-request transport authentication. Defaults to `Auth.passthrough`. */ + /** + * Per-request transport authentication. Defaults to `Auth.bearer`, which + * sets `Authorization: Bearer ` 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 @@ -177,7 +183,7 @@ export interface FromProtocolInput { export function fromProtocol( input: FromProtocolInput, ): AdapterDefinition { - const auth = input.auth ?? authPassthrough + const auth = input.auth ?? authBearer const protocol = input.protocol const buildHeaders = input.headers ?? (() => ({})) diff --git a/packages/llm/src/auth.ts b/packages/llm/src/auth.ts index c1b0b2aa70..5b3474abfe 100644 --- a/packages/llm/src/auth.ts +++ b/packages/llm/src/auth.ts @@ -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, diff --git a/packages/llm/src/provider/openai-chat.ts b/packages/llm/src/provider/openai-chat.ts index 6b5a38cbbe..aca8c284f2 100644 --- a/packages/llm/src/provider/openai-chat.ts +++ b/packages/llm/src/provider/openai-chat.ts @@ -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, }) diff --git a/packages/llm/src/provider/openai-compatible-chat.ts b/packages/llm/src/provider/openai-compatible-chat.ts index 580cded039..bbc6172c84 100644 --- a/packages/llm/src/provider/openai-compatible-chat.ts +++ b/packages/llm/src/provider/openai-compatible-chat.ts @@ -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, }) diff --git a/packages/llm/src/provider/openai-responses.ts b/packages/llm/src/provider/openai-responses.ts index 5b48e0ebf8..1dd8474b4d 100644 --- a/packages/llm/src/provider/openai-responses.ts +++ b/packages/llm/src/provider/openai-responses.ts @@ -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, })