Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/anthropic-client-injection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/ai-anthropic': minor
---

Add a client-injection factory for Anthropic-compatible clients with custom
authentication and transport.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ Official adapters include:
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
| [`@tanstack/ai-openrouter`](https://tanstack.com/ai/latest/docs/adapters/openrouter) | 300+ models through one OpenRouter API, with per-request cost tracking |
| [`@tanstack/ai-openai`](https://tanstack.com/ai/latest/docs/adapters/openai) | OpenAI chat, image, video, speech, transcription, realtime, and provider tools |
| [`@tanstack/ai-anthropic`](https://tanstack.com/ai/latest/docs/adapters/anthropic) | Anthropic Claude chat, thinking, tools, and structured outputs |
| [`@tanstack/ai-anthropic`](https://tanstack.com/ai/latest/docs/adapters/anthropic) | Anthropic Claude chat, thinking, tools, structured outputs, and custom clients |
| [`@tanstack/ai-gemini`](https://tanstack.com/ai/latest/docs/adapters/gemini) | Google Gemini chat, image, speech, and audio generation |
| [`@tanstack/ai-ollama`](https://tanstack.com/ai/latest/docs/adapters/ollama) | Local Ollama models |
| [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime |
Expand Down
52 changes: 50 additions & 2 deletions docs/adapters/anthropic.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,46 @@ const config: Omit<AnthropicTextConfig, "apiKey"> = {

const adapter = createAnthropicChat("claude-sonnet-4-6", process.env.ANTHROPIC_API_KEY!, config);
```


## Custom Anthropic Client

Use `createAnthropicChatWithClient` when authentication or transport is
provided by an Anthropic-compatible client. The adapter only requires the
client's `beta.messages.create` capability; message mapping, streaming, tools,
media, usage, and structured output still follow the same TanStack adapter
path.

For example, Anthropic's Vertex client can discover Google Cloud credentials
through Application Default Credentials:

```bash
npm install @tanstack/ai-anthropic @anthropic-ai/vertex-sdk
```

```typescript
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk";
import {
createAnthropicChatWithClient,
type AnthropicMessagesClient,
} from "@tanstack/ai-anthropic";

const projectId = process.env.GOOGLE_CLOUD_PROJECT;

if (!projectId) {
throw new Error("GOOGLE_CLOUD_PROJECT is required");
}

const client = new AnthropicVertex({
projectId,
region: "eu",
}) satisfies AnthropicMessagesClient;

const adapter = createAnthropicChatWithClient("claude-sonnet-5", client);
```

The injected client must implement the Anthropic Beta Messages protocol.
Endpoint-specific model and feature support remains the caller's
responsibility.

## Example: Chat Completion

Expand Down Expand Up @@ -257,7 +296,7 @@ ANTHROPIC_API_KEY=sk-ant-...

## API Reference

Every factory pair follows the same shape: the short factory (`anthropicText`, `anthropicSummarize`) reads `ANTHROPIC_API_KEY` from the environment, while `createAnthropicChat` / `createAnthropicSummarize` take an explicit API key. Both take `model` as the first argument.
Every factory pair follows the same shape: the short factory (`anthropicText`, `anthropicSummarize`) reads `ANTHROPIC_API_KEY` from the environment, while `createAnthropicChat` / `createAnthropicSummarize` take an explicit API key. Both take `model` as the first argument. For custom authentication or transport, `createAnthropicChatWithClient` accepts an Anthropic-compatible Messages client instead.

### `anthropicText(model, config?)` / `createAnthropicChat(model, apiKey, config?)`

Expand All @@ -268,6 +307,15 @@ Creates an Anthropic chat adapter.
- `model` - Claude model id (e.g. `"claude-sonnet-5"`, `"claude-fable-5"`, `"claude-opus-4-8"`)
- `config?.baseURL` - Custom base URL (optional)

### `createAnthropicChatWithClient(model, client)`

Creates an Anthropic chat adapter using an injected client.

**Parameters:**

- `model` - Claude model id
- `client` - Client exposing `beta.messages.create`

### `anthropicSummarize(model, config?)` / `createAnthropicSummarize(model, apiKey, config?)`

Creates an Anthropic summarization adapter.
Expand Down
2 changes: 1 addition & 1 deletion docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@
"label": "Anthropic",
"to": "adapters/anthropic",
"addedAt": "2026-04-15",
"updatedAt": "2026-07-04"
"updatedAt": "2026-07-23"
},
{
"label": "Google Gemini",
Expand Down
2 changes: 2 additions & 0 deletions packages/ai-anthropic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
"zod": "^4.0.0"
},
"devDependencies": {
"@anthropic-ai/sdk-v112": "npm:@anthropic-ai/sdk@0.112.4",
"@anthropic-ai/vertex-sdk": "^0.19.0",
"@tanstack/ai": "workspace:*",
"@vitest/coverage-v8": "4.0.14",
"zod": "^4.2.0"
Expand Down
53 changes: 49 additions & 4 deletions packages/ai-anthropic/src/adapters/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ import type {
AnthropicMessageMetadataByModality,
AnthropicTextMetadata,
} from '../message-types'
import type { AnthropicClientConfig } from '../utils/client'
import type {
AnthropicClientConfig,
AnthropicMessagesClient,
} from '../utils/client'

/**
* The block type carried by an Anthropic provider-executed (server) tool's
Expand Down Expand Up @@ -202,6 +205,10 @@ export function computeAnthropicBetas(
*/
export interface AnthropicTextConfig extends AnthropicClientConfig {}

export type AnthropicTextAdapterConfig =
| AnthropicTextConfig
| { client: AnthropicMessagesClient }

/**
* Anthropic-specific provider options for text/chat
*/
Expand Down Expand Up @@ -234,6 +241,24 @@ type ResolveToolCapabilities<TModel extends string> =
? NonNullable<AnthropicChatModelToolCapabilitiesByName[TModel]>
: readonly []

type SdkAnthropicMessagesClient = {
beta: {
messages: Pick<Anthropic_SDK['beta']['messages'], 'create'>
}
}

/**
* Restore the package SDK's precise overloads at the adapter boundary.
* Alternative clients may use a separate Anthropic 0.x SDK whose declarations
* drift while implementing the same Messages protocol at runtime.
*/
function asSdkAnthropicMessagesClient(
client: AnthropicMessagesClient,
): SdkAnthropicMessagesClient {
// oxlint-disable-next-line eslint-js/no-restricted-syntax -- The public callable deliberately erases version-specific SDK overloads; restore this package's SDK type at the internal boundary.
return client as unknown as SdkAnthropicMessagesClient
}

// ===========================
// Adapter Implementation
// ===========================
Expand Down Expand Up @@ -266,11 +291,14 @@ export class AnthropicTextAdapter<
override readonly kind = 'text' as const
readonly name = 'anthropic' as const

private readonly client: Anthropic_SDK
private readonly client: SdkAnthropicMessagesClient

constructor(config: AnthropicTextConfig, model: TModel) {
constructor(config: AnthropicTextAdapterConfig, model: TModel) {
super({}, model)
this.client = createAnthropicClient(config)
this.client =
'client' in config
? asSdkAnthropicMessagesClient(config.client)
: createAnthropicClient(config)
}

async *chatStream(
Expand Down Expand Up @@ -1468,6 +1496,23 @@ export function createAnthropicChat<
return new AnthropicTextAdapter({ apiKey, ...config }, model)
}

/**
* Creates an Anthropic chat adapter with an injected Messages client.
* Type resolution happens here at the call site.
*/
export function createAnthropicChatWithClient<
TModel extends (typeof ANTHROPIC_MODELS)[number],
>(
model: TModel,
client: AnthropicMessagesClient,
): AnthropicTextAdapter<
TModel,
ResolveProviderOptions<TModel>,
ResolveInputModalities<TModel>
> {
return new AnthropicTextAdapter({ client }, model)
}

/**
* Creates an Anthropic text adapter with automatic API key detection.
* Type resolution happens here at the call site.
Expand Down
3 changes: 3 additions & 0 deletions packages/ai-anthropic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ export {
AnthropicTextAdapter,
anthropicText,
createAnthropicChat,
createAnthropicChatWithClient,
type AnthropicTextAdapterConfig,
type AnthropicTextConfig,
type AnthropicTextProviderOptions,
} from './adapters/text'
export type { AnthropicMessagesClient } from './utils/client'
export type { AnthropicSystemPromptMetadata } from './text/text-provider-options'

// Summarize - thin factory functions over @tanstack/ai's ChatStreamSummarizeAdapter
Expand Down
21 changes: 21 additions & 0 deletions packages/ai-anthropic/src/utils/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ export interface AnthropicClientConfig extends ClientOptions {
apiKey: string
}

type AnyAnthropicMessagesCreate = (
params: never,
...args: Array<never>
) => unknown

/**
* The minimal Anthropic client surface used by the text adapter.
*
* The callable is intentionally type-erased because alternative Anthropic
* clients can depend on a different 0.x release of the Anthropic SDK. Their
* request and response declarations may drift even when the runtime Messages
* protocol remains compatible.
*/
export interface AnthropicMessagesClient {
readonly beta: {
readonly messages: {
readonly create: AnyAnthropicMessagesCreate
}
}
}

/**
* Creates an Anthropic SDK client instance
*/
Expand Down
30 changes: 30 additions & 0 deletions packages/ai-anthropic/tests/anthropic-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type StreamChunk,
type UIMessage,
} from '@tanstack/ai'
import { createAnthropicChatWithClient } from '../src'
import { AnthropicTextAdapter } from '../src/adapters/text'
import type { AnthropicTextProviderOptions } from '../src/adapters/text'
import { ANTHROPIC_MAX_NONSTREAMING_TOKENS } from '../src/model-meta'
Expand Down Expand Up @@ -77,6 +78,35 @@ function createTextStream(text: string) {
})()
}

describe('Anthropic client injection', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('uses the injected messages client instead of constructing one', async () => {
const create = vi.fn().mockResolvedValueOnce(createTextStream('Hello'))
const client = {
beta: {
messages: {
create,
},
},
}

const adapter = createAnthropicChatWithClient('claude-opus-4-1', client)

for await (const _ of chat({
adapter,
messages: [{ role: 'user', content: 'Hi' }],
})) {
// consume stream
}

expect(create).toHaveBeenCalledOnce()
expect(mocks.betaMessagesCreate).not.toHaveBeenCalled()
})
})

describe('Anthropic adapter option mapping', () => {
beforeEach(() => {
vi.clearAllMocks()
Expand Down
18 changes: 18 additions & 0 deletions packages/ai-anthropic/tests/client-injection-type-safety.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { expectTypeOf, it } from 'vitest'
import type AnthropicSdkV112 from '@anthropic-ai/sdk-v112'
import type { AnthropicVertex } from '@anthropic-ai/vertex-sdk'
import type { AnthropicMessagesClient } from '../src'

type V112MessagesClient = {
readonly beta: {
readonly messages: Pick<AnthropicSdkV112['beta']['messages'], 'create'>
}
}

it('accepts the official Vertex client', () => {
expectTypeOf<AnthropicVertex>().toExtend<AnthropicMessagesClient>()
})

it('accepts clients backed by a newer Anthropic SDK version', () => {
expectTypeOf<V112MessagesClient>().toExtend<AnthropicMessagesClient>()
})
43 changes: 43 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions testing/e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"postinstall": "playwright install chromium"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.97.1",
"@copilotkit/aimock": "^1.34.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@openrouter/sdk": "0.13.20",
Expand Down
Loading