# Use the AI SDK you already have (/docs/guides/existing-sdks)

A site that already calls a model through an AI SDK keeps that code. `AiConnect` exposes a `fetch` that the SDKs accept in their options. The SDK builds its request as usual, `aiconnect.fetch` hands it to the visitor's wallet, and the answer comes back in the format the SDK expects, streamed or whole. Your tool loop, your structured output and your error handling stay as they are.

## How it works [#how-it-works]

1. The SDK calls `aiconnect.fetch` in place of the network. The path of the request tells which protocol the SDK speaks: OpenAI chat completions, the OpenAI Responses API, Anthropic Messages or Gemini `generateContent`.
2. `aiconnect.fetch` reads the request into the wallet's chat format: the messages and their images, the tool definitions, the tool choice, the response format and the generation settings.
3. The wallet answers with the account the visitor picked for your site, on the model picked for it. That account can belong to any provider: a Claude plan can answer an OpenAI SDK request, and a DeepSeek key can answer an Anthropic SDK request.
4. The text streams back the way the provider's own servers stream it. The tool calls, the finish reason and the token counts follow when the answer ends.

No request reaches the address the SDK targets. The SDKs require an API key, so `sdkOptions` passes the placeholder `aiwallet`, which the wallet never reads. The `model` your code names is not used either: the visitor chooses the model in the wallet, and each answer reports the model that answered.

`aiconnect.fetch` answers in the browser only. A request it receives while the client is still checking your site waits for that check. A request from a site the visitor has not connected fails with status 401, so show your connect entry point first, as the [React quickstart](/docs/quickstart/react) does.

## The change for each SDK [#the-change-for-each-sdk]

Each section shows the constructor before and after, then the code the bookshop demo in `apps/demo` of the AI Connect repository runs in its SDK mode with the visitor's account. The demo's catalog tool `search_books` stands for your own tools, and `aiconnect` is your `AiConnect` client.

### OpenAI SDK [#openai-sdk]

```ts
// Before
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

// After
const client = new OpenAI(aiconnect.sdkOptions);
```

`sdkOptions` is `{ apiKey: 'aiwallet', fetch: aiconnect.fetch, dangerouslyAllowBrowser: true }`. The OpenAI SDK runs in a browser only with `dangerouslyAllowBrowser`, a guard meant for real keys, and no key reaches the page here. Chat completions and the Responses API both answer, streamed or not.

```ts
import OpenAI from 'openai';

const client = new OpenAI(aiconnect.sdkOptions);
const messages: OpenAI.ChatCompletionMessageParam[] = [
  { role: 'system', content: system },
  { role: 'user', content: question },
];
for (let step = 0; step < 4; step += 1) {
  const stream = client.chat.completions.stream({ model: 'gpt-6-luna', messages, tools }, { signal });
  stream.on('content', (delta) => showText(delta));
  const { message } = (await stream.finalChatCompletion()).choices[0];
  messages.push(message);
  const calls = (message.tool_calls ?? []).filter((call) => call.type === 'function');
  if (!calls.length) {
    break;
  }
  for (const call of calls) {
    const content = await runTool(call.function.name, JSON.parse(call.function.arguments || '{}'));
    messages.push({ role: 'tool', tool_call_id: call.id, content });
  }
}
```

### Anthropic SDK [#anthropic-sdk]

```ts
// Before
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

// After
const client = new Anthropic(aiconnect.sdkOptions);
```

```ts
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic(aiconnect.sdkOptions);
const messages: Anthropic.MessageParam[] = [{ role: 'user', content: question }];
for (let step = 0; step < 4; step += 1) {
  const stream = client.messages.stream(
    { model: 'claude-sonnet-5-5', max_tokens: 2048, system, messages, tools },
    { signal },
  );
  stream.on('text', (delta) => showText(delta));
  const message = await stream.finalMessage();
  messages.push({ role: 'assistant', content: message.content });
  const uses = message.content.filter((block) => block.type === 'tool_use');
  if (!uses.length) {
    break;
  }
  const results: Anthropic.ToolResultBlockParam[] = [];
  for (const use of uses) {
    results.push({ type: 'tool_result', tool_use_id: use.id, content: await runTool(use.name, use.input) });
  }
  messages.push({ role: 'user', content: results });
}
```

### Google Gen AI [#google-gen-ai]

```ts
// Before
const client = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

// After
const client = new GoogleGenAI({ apiKey: aiconnect.sdkOptions.apiKey, httpOptions: { fetch: aiconnect.fetch } });
```

```ts
import { GoogleGenAI } from '@google/genai';
import type { Content, Part } from '@google/genai';

const client = new GoogleGenAI({ apiKey: aiconnect.sdkOptions.apiKey, httpOptions: { fetch: aiconnect.fetch } });
const config = { systemInstruction: system, tools: [{ functionDeclarations }], abortSignal: signal };
const contents: Content[] = [{ role: 'user', parts: [{ text: question }] }];
for (let step = 0; step < 4; step += 1) {
  const parts: Part[] = [];
  for await (const chunk of await client.models.generateContentStream({ model: 'gemini-3-pro', contents, config })) {
    for (const part of chunk.candidates?.[0]?.content?.parts ?? []) {
      if (part.text && !part.thought) {
        showText(part.text);
      }
      parts.push(part);
    }
  }
  contents.push({ role: 'model', parts });
  const calls = parts.flatMap((part) => (part.functionCall ? [part.functionCall] : []));
  if (!calls.length) {
    break;
  }
  const responses: Part[] = [];
  for (const { id, name = '', args = {} } of calls) {
    responses.push({ functionResponse: { id, name, response: { output: await runTool(name, args) } } });
  }
  contents.push({ role: 'user', parts: responses });
}
```

Gemini may leave tool call ids out. The wallet then numbers the calls in order, and each function response answers the first open call of its name.

### Vercel AI SDK [#vercel-ai-sdk]

```ts
// Before
import { openai } from '@ai-sdk/openai';

// After
import { createOpenAI } from '@ai-sdk/openai';

const openai = createOpenAI({ apiKey: aiconnect.sdkOptions.apiKey, fetch: aiconnect.fetch });
```

`openai(model)` speaks the Responses API and `openai.chat(model)` speaks chat completions. `createAnthropic` from `@ai-sdk/anthropic` and `createGoogleGenerativeAI` from `@ai-sdk/google` take the same two options. `streamText` and `generateText` run their tool loop and their structured output unchanged.

```ts
import { createOpenAI } from '@ai-sdk/openai';
import { jsonSchema, stepCountIs, streamText, tool } from 'ai';

const openai = createOpenAI({ apiKey: aiconnect.sdkOptions.apiKey, fetch: aiconnect.fetch });
const result = streamText({
  model: openai('gpt-6-luna'),
  system,
  prompt: question,
  tools: {
    search_books: tool({
      description: 'Find books in the catalog by mood and maximum length.',
      inputSchema: jsonSchema(searchBooksSchema),
      execute: (args) => runTool('search_books', args),
    }),
  },
  stopWhen: stepCountIs(4),
  abortSignal: signal,
});
for await (const part of result.fullStream) {
  if (part.type === 'text-delta') {
    showText(part.text);
  } else if (part.type === 'error') {
    throw part.error;
  }
}
```

### LangChain [#langchain]

```ts
// Before
const model = new ChatOpenAI({ model: 'gpt-6-luna', apiKey: process.env.OPENAI_API_KEY });

// After
const model = new ChatOpenAI({
  model: 'gpt-6-luna',
  apiKey: aiconnect.sdkOptions.apiKey,
  configuration: aiconnect.sdkOptions,
});
```

```ts
import { HumanMessage, SystemMessage, ToolMessage } from '@langchain/core/messages';
import type { AIMessageChunk, BaseMessage } from '@langchain/core/messages';
import { ChatOpenAI } from '@langchain/openai';

const { sdkOptions } = aiconnect;
const model = new ChatOpenAI({ apiKey: sdkOptions.apiKey, model: 'gpt-6-luna', configuration: sdkOptions })
  .bindTools(tools);
const messages: BaseMessage[] = [new SystemMessage(system), new HumanMessage(question)];
for (let step = 0; step < 4; step += 1) {
  let answer: AIMessageChunk | undefined;
  for await (const chunk of await model.stream(messages, { signal })) {
    if (typeof chunk.content === 'string' && chunk.content) {
      showText(chunk.content);
    }
    answer = answer ? answer.concat(chunk) : chunk;
  }
  if (!answer) {
    break;
  }
  messages.push(answer);
  if (!answer.tool_calls?.length) {
    break;
  }
  for (const call of answer.tool_calls) {
    const id = call.id ?? call.name;
    messages.push(new ToolMessage({ tool_call_id: id, content: await runTool(call.name, call.args) }));
  }
}
```

### LlamaIndex [#llamaindex]

```ts
// Before
const llm = new OpenAI({ model: 'gpt-6-luna', apiKey: process.env.OPENAI_API_KEY });

// After
const llm = new OpenAI({
  model: 'gpt-6-luna',
  apiKey: aiconnect.sdkOptions.apiKey,
  additionalSessionOptions: aiconnect.sdkOptions,
});
```

`OpenAI` is the LLM class of `@llamaindex/openai`, which hands `additionalSessionOptions` to the OpenAI SDK client it builds. LlamaIndex gives that client no abort signal, so the demo passes a `fetch` bound to its own signal, which Stop aborts. The demo runs its tool loop on `llm.chat`: the agent of `@llamaindex/workflow` runs on the same LLM, but a call that fails there is left unhandled and the agent's stream never ends. The LlamaIndex packages for TypeScript are marked deprecated on npm since June 2026; the demo runs on their last releases.

```ts
import type {
  BaseTool, ChatMessage, PartialToolCall, ToolCall, ToolCallLLMMessageOptions,
} from '@llamaindex/core/llms';
import { OpenAI } from '@llamaindex/openai';

const { sdkOptions } = aiconnect;
const fetch: typeof aiconnect.fetch = (input, init) => (
  aiconnect.fetch(input, { ...init, signal: AbortSignal.any(init?.signal ? [signal, init.signal] : [signal]) })
);
const llm = new OpenAI({
  model: 'gpt-6-luna', apiKey: sdkOptions.apiKey, additionalSessionOptions: { ...sdkOptions, fetch },
});
const tools: BaseTool[] = [{ metadata: { name: 'search_books', description, parameters: searchBooksSchema } }];
// A call's input streams as text, then comes once more parsed when the call is complete.
const isComplete = (call: ToolCall | PartialToolCall): call is ToolCall => typeof call.input !== 'string';
const messages: ChatMessage<ToolCallLLMMessageOptions>[] = [
  { role: 'system', content: system },
  { role: 'user', content: question },
];
for (let step = 0; step < 4; step += 1) {
  let content = '';
  const calls = new Map<string, ToolCall>();
  for await (const chunk of await llm.chat({ messages, tools, stream: true })) {
    showText(chunk.delta);
    content += chunk.delta;
    const options = chunk.options ?? {};
    ('toolCall' in options ? options.toolCall : []).filter(isComplete).forEach((call) => calls.set(call.id, call));
  }
  if (!calls.size) {
    break;
  }
  messages.push({ role: 'assistant', content, options: { toolCall: [...calls.values()] } });
  for (const call of calls.values()) {
    const result = await runTool(call.name, call.input);
    messages.push({ role: 'user', content: result, options: { toolResult: { id: call.id, result, isError: false } } });
  }
}
```

`OpenAI` from `@llamaindex/openai` retries a failed call ten times by default, so a spent plan keeps the visitor waiting; `maxRetries: 0` answers at once (see [Errors](#errors)).

### Mastra [#mastra]

A Mastra agent cannot run in the page. `@mastra/core` reads `process.env`, the file system and Node's `AsyncLocalStorage` as it loads, and a browser has none of them, while the wallet answers in the page only. A site built on Mastra keeps its tools on its server and serves them over MCP with `MCPServer` from `@mastra/mcp`. The page runs the conversation with the Vercel AI SDK, the SDK Mastra's agents are built on, and reads those tools as the [MCP guide](/docs/guides/mcp-tools#with-an-ai-sdk) shows.

```ts
// On the server: the agent's tools, served at /api/mcp
import type { IncomingMessage, ServerResponse } from 'node:http';
import { MCPServer } from '@mastra/mcp';

const server = new MCPServer({ name: 'bookshop', version: '1.0.0', tools: { searchBooks } });

export async function handleMcp(req: IncomingMessage, res: ServerResponse) {
  await server.startHTTP({ url: new URL(req.url ?? '/', 'http://localhost'), httpPath: '/api/mcp', req, res });
}
```

The tools keep the `createTool` definitions the agent used. The agent's instructions become the `system` prompt of the page's `streamText`.

### Other libraries [#other-libraries]

A library built on one of these SDKs works when it lets you pass the SDK's options or its `fetch`, and when it runs in a browser.

## Move a call from your server into the page [#move-a-call-from-your-server-into-the-page]

Most sites call the model from a server route, because an API key cannot reach the browser. The wallet needs no key, so the call moves into the page and the route goes away. What the route did with your data, such as a catalog search, stays on your server as tools on an MCP server, which the model calls from the page. The [MCP guide](/docs/guides/mcp-tools#with-an-ai-sdk) shows how to hand those tools to the SDK.

```ts
// Before: app/api/chat/route.ts, on your server, with your key
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';

export async function POST(request: Request) {
  const { messages } = await request.json();
  const result = streamText({ model: openai('gpt-6-luna'), system, messages, tools: catalogTools });
  return result.toUIMessageStreamResponse();
}
```

```ts
// After: in a client component, on the visitor's account
'use client';

import { createOpenAI } from '@ai-sdk/openai';
import { streamText } from 'ai';

const openai = createOpenAI({ apiKey: aiconnect.sdkOptions.apiKey, fetch: aiconnect.fetch });
const result = streamText({ model: openai('gpt-6-luna'), system, prompt: question, tools: catalogTools });
```

The [Next.js quickstart](/docs/quickstart/nextjs) shows where the `AiConnect` client lives in a Next.js app.

## What the wallet takes from a request [#what-the-wallet-takes-from-a-request]

| In the request | What the wallet does |
| --- | --- |
| Messages, system prompt and images | Sends them to the account. An image as a `data:` URL reaches every account. An image as an https URL reaches the accounts whose provider fetches images; the Gemini plan refuses it. |
| Tools and tool choice | Sends them to the account. Claude Opus 5.5, Claude Sonnet 5.5 and Claude Fable 5.1 refuse a forced choice (`required` or a named tool), and the provider's error comes back to the SDK. |
| Response format, JSON or a JSON schema | Sends it in the format of the account's provider. |
| Maximum tokens, `temperature`, `top_p`, stop sequences | Sends each one to the accounts that take it and leaves it out for the others, as the [providers page](/docs/providers#request-options-each-account-leaves-out) lists for each account. |
| `model` | Not used. The visitor's choice in the wallet decides. |
| Reasoning effort and thinking settings | Not used. They stay the visitor's choice. |

A request cannot carry:

- `previous_response_id` on the Responses API, since the wallet keeps no conversation: send the whole input;
- more than one answer per request (`n` above 1, `candidateCount` above 1);
- files, documents or audio in a message, since images are the only media the wallet passes on;
- a provider's own server tools, such as its web search or code execution, since the wallet passes on function tools only.

## Errors [#errors]

A failure before any text comes back as an HTTP error with the wallet's status, in the error format of the SDK's provider, so each SDK raises its own error class: `OpenAI.RateLimitError` for a 429, `Anthropic.AuthenticationError` for a 401, and so on.

| Status | When |
| --- | --- |
| 400 | The request carries something the wallet does not take (see above). |
| 401 | The site is not connected, the wallet is not installed, or the code runs on a server. |
| 403 | The visitor declined the site, left the wallet locked, or has no account to answer. |
| 404 | An endpoint the wallet does not answer: embeddings, images, audio, model lists. |
| 410 | The wallet was updated after the page opened, and the page needs a reload. |
| 429 and other provider statuses | The provider refused the call, for example when a plan is spent. |
| 499 | The visitor stopped the answer in the wallet. |
| 502 | The wallet could not reach the provider. |

A failure after the text started arrives as the protocol's error event with OpenAI and Anthropic, and ends the stream with Gemini. Aborting the SDK's request, with its `signal` or by breaking out of its stream, stops the answer in the wallet.

The OpenAI and Anthropic SDKs retry a 429 and the 5xx statuses twice by default, and LlamaIndex's `OpenAI` ten times. The wallet already tries the visitor's other accounts when the site is on Auto, so you can pass `maxRetries: 0` to answer sooner.

## Model names [#model-names]

Each answer names the model that answered, from the first event of its stream. Before the first text, the wallet tells the page which account answers and the model it asks that account for, so a site on Auto reads the model of the account the wallet picked: Anthropic's `message_start`, the Responses API's `response.created` and the first chunks of chat completions and Gemini carry it. The events that end the stream, and a whole answer, carry the model as the provider named it, which can be more precise, such as a dated version.
