Cloud Providers
OpenAI
The simplest adapter: it's a thin wrapper over the shared openai-compatible factory (the same one Ollama and vLLM use), pointed at OpenAI's own endpoint — since OpenAI's API is definitionally OpenAI-shaped.
| Endpoint | https://api.openai.com/v1/chat/completions |
| Auth | authorization: Bearer <OPENAI_API_KEY> |
| Env var | OPENAI_API_KEY |
Anthropic
A bespoke adapter, since the Messages API isn't OpenAI-shaped.
| Endpoint | https://api.anthropic.com/v1/messages |
| Auth | x-api-key: <ANTHROPIC_API_KEY>, anthropic-version: 2023-06-01 |
| Env var | ANTHROPIC_API_KEY |
Request mapping: Anthropic has no system role in its messages array — any system messages in the incoming request are pulled out and joined (\n\n) into a top-level system field instead. max_tokens defaults to 4096 if the client didn't set one (Anthropic requires it; OpenAI-shaped APIs don't). stop is normalized into Anthropic's stop_sequences.
Response mapping: all content blocks of type text are concatenated into the single message.content string Lattice's response shape expects. stop_reason: 'max_tokens' maps to finish_reason: 'length'; everything else maps to 'stop'.
Streaming: Anthropic's stream is a sequence of named SSE events, not self-contained chunks, so the stream adapter is a small stateful reducer: message_start emits the initial role: 'assistant' chunk and captures input_tokens for later; content_block_delta (text deltas) emits content chunks; message_delta emits the terminal chunk carrying finish_reason and usage (using the input_tokens captured earlier, since Anthropic only reports output-token deltas incrementally); content_block_start/stop, ping, and message_stop produce no chunk.
Google (Gemini)
Also bespoke — the most structurally different of the three.
| Endpoint | https://generativelanguage.googleapis.com/v1beta/models/{providerModel}:{generateContent|streamGenerateContent}?key=<GOOGLE_API_KEY>[&alt=sse] |
| Auth | API key as a query parameter, not a header |
| Env var | GOOGLE_API_KEY |
Unlike the other two, the model name is a URL path segment, not a body field — providerModel gets interpolated directly into the endpoint URL, and the method name itself (generateContent vs. streamGenerateContent) changes based on whether the request is streaming.
Request mapping: assistant role maps to Gemini's model role. System messages are joined into systemInstruction.parts[0].text. temperature, top_p, max_tokens, and stop map into generationConfig.{temperature, topP, maxOutputTokens, stopSequences}.
Response mapping: finishReason: 'MAX_TOKENS' → 'length'; finishReason: 'SAFETY' → 'content_filter'; everything else → 'stop'. Gemini responses carry no response id, so one is synthesized: `gemini-${Bun.randomUUIDv7()}`.
Streaming: unlike Anthropic, each Gemini stream chunk is self-contained (the full shape, not an incremental delta against prior state), so the stream mapper is a stateless per-chunk transform rather than a reducer. usageMetadata only appears on the terminal chunk.
At a glance
| OpenAI | Anthropic | ||
|---|---|---|---|
| Adapter kind | openai-compatible factory | Bespoke | Bespoke |
| Auth transport | Header | Header (x-api-key) | Query param |
| System role | Native | Merged into system | Merged into systemInstruction |
| Stream shape | Deltas | Named events, stateful reduce | Self-contained chunks, stateless map |
Synthesized id | No (upstream provides one) | No | Yes |