# @aiconnect/core (/docs/reference/core)

`@aiconnect/core` is the site's side of AI Connect, without any framework. It exports the `AiConnect` client, the `McpToolSource` class, the `runConversation` and `finalAnswer` functions, the chat content helpers, `WalletRequestError`, and the types they share with AI Wallet.

The tables on this page are generated from the package's TypeScript source each time the docs are built. Open a row to see its full type.

```ts
import {
  AiConnect, McpToolSource, WalletRequestError, contentParts, dataUrl, finalAnswer, inlineData, runConversation, textOf,
} from '@aiconnect/core';
import type {
  ChatEvent, ChatMessage, ChatResponse, ChatStart, ConnectionState, ContentPart, GenerationHint, JsonSchema, ToolSource,
  Usage, UserMessage, WalletChatParams, WalletLink, WalletProvider,
} from '@aiconnect/core';
```

## AiConnect [#aiconnect]

The site's connection to the visitor's AI, through AI Wallet. Its public members:

### AiConnectMembers

| Prop         | Type                                 | Description |
| ------------ | ------------------------------------ | ----------- |
| `installUrl` | `string`                             |             |
| `fetch`      | `WalletFetch`                        |             |
| `sdkOptions` | `SdkOptions`                         |             |
| `state`      | `ConnectionState`                    |             |
| `subscribe`  | `(listener: Listener) => () => void` |             |
| `connect`    | `() => Promise<void>`                |             |
| `disconnect` | `() => Promise<void>`                |             |

- **constructor.** `new AiConnect()` takes no options. In a browser, the client looks for the wallet at once and asks for the site's status. A site the visitor already approved becomes `connected` without a prompt. Built where `window` does not exist, during a server render, the client does nothing and stays `checking`.
- **installUrl.** The AI Wallet page, https://tensorpass.app/wallet, where a visitor without the wallet gets it. The connect window of `@aiconnect/react` links to it in the `noWallet` state.
- **state.** The current connection state.
- **subscribe(listener).** Calls `listener` with each new state and returns a function that removes it. The listener is not called with the current state when you subscribe, so read `state` first.
- **connect().** Asks the wallet to let this site use the visitor's AI. Without a wallet the state becomes `noWallet`. Otherwise it moves to `connecting`, then to the state the wallet's answer gives, usually `connected`, or to `error` with the wallet's reason, such as "The connection was declined". The promise resolves in each of these cases. A locked wallet first asks the visitor to unlock it, and the wallet asks the visitor to approve the site the first time it connects.
- **disconnect().** Asks the wallet to remove this site from the visitor's approved sites, then sets the state to `disconnected`, or to `noWallet` without a wallet. The next `connect()` asks the visitor again. The promise rejects when the wallet refuses, for example "The wallet is locked".
- **fetch.** A `fetch` for the AI SDKs, which answers their requests through the wallet in their own protocol. See [WalletFetch](#walletfetch).
- **sdkOptions.** The options an OpenAI or Anthropic SDK client takes to answer through the wallet. See [SdkOptions](#sdkoptions).

The state also changes on its own when the visitor acts in the wallet: picking another account for the site, removing the site, or unlocking the wallet.

```ts
const client = new AiConnect();

client.subscribe((state) => {
  if (state.status === 'connected') {
    console.log(`Answered by ${state.providerName}`);
  }
});

document.querySelector('#connect')?.addEventListener('click', () => client.connect());
```

## ConnectionState [#connectionstate]

A union of six states, one per `status`. Each row holds the shape of the state with that status.

### ConnectionStates

| Prop           | Type                                                                                       | Description |
| -------------- | ------------------------------------------------------------------------------------------ | ----------- |
| `checking`     | `{ status: 'checking'; }`                                                                  |             |
| `connected`    | `{ status: 'connected'; providerName: string; model?: string; session: ProviderSession; }` |             |
| `connecting`   | `{ status: 'connecting'; }`                                                                |             |
| `disconnected` | `{ status: 'disconnected'; }`                                                              |             |
| `error`        | `{ status: 'error'; message: string; }`                                                    |             |
| `noWallet`     | `{ status: 'noWallet'; }`                                                                  |             |

| Status | Meaning |
| --- | --- |
| `checking` | The client is looking for the wallet, or the page is rendering on a server. |
| `noWallet` | No wallet answered within 300 milliseconds of the page asking for one. |
| `disconnected` | The wallet answered, and this site is not connected. A wallet that is locked when the page loads reports the site this way until the visitor unlocks it. |
| `connecting` | `connect()` is waiting for the visitor in the wallet. |
| `connected` | The site may chat. `providerName` is the display name of the account that answers it, or `Auto`. `model` is that account's default model, absent with Auto. `session` sends chats to the wallet. |
| `error` | The last `connect()` failed. `message` holds the reason. |

```ts
function label(state: ConnectionState): string {
  switch (state.status) {
    case 'connected':
      return state.model ? `${state.providerName} (${state.model})` : state.providerName;
    case 'error':
      return state.message;
    default:
      return state.status;
  }
}
```

## SdkOptions [#sdkoptions]

The type of `client.sdkOptions`: the options an OpenAI or Anthropic SDK client takes to answer through the wallet.

### SdkOptions

| Prop                      | Type          | Description |
| ------------------------- | ------------- | ----------- |
| `apiKey`                  | `string`      |             |
| `fetch`                   | `WalletFetch` |             |
| `dangerouslyAllowBrowser` | `true`        |             |

- **apiKey.** A placeholder, since the SDKs require a key. The wallet does not read it.
- **fetch.** The client's [`fetch`](#walletfetch).
- **dangerouslyAllowBrowser.** `true`. The SDKs guard real keys from the browser, and no real key reaches the page.

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

const openai = new OpenAI(client.sdkOptions);
const anthropic = new Anthropic(client.sdkOptions);
```

## WalletFetch [#walletfetch]

The type of `client.fetch`, a function with the signature of the global `fetch`:

### WalletFetchSignature

| Prop         | Type                                                           | Description |
| ------------ | -------------------------------------------------------------- | ----------- |
| `parameters` | `[input: URL \| RequestInfo, init?: RequestInit \| undefined]` |             |
| `returns`    | `Promise<Response>`                                            |             |

It lets the AI SDK a site already uses answer through the visitor's AI. The request stays in the page: `fetch` reads neither the host the SDK targets nor its API key.

- It answers `POST` requests whose path ends with `/chat/completions` (OpenAI Chat Completions), `/responses` (OpenAI Responses), `/messages` (Anthropic Messages), or `:generateContent` and `:streamGenerateContent` (Gemini). Any other request gets a 404 with a JSON error in the shape the OpenAI, Anthropic and Google SDKs read.
- It reads the request into the wallet's chat format, asks the wallet, and answers in the same protocol, streamed or whole. The wallet answers with the account the visitor picked for the site, and the response names the model that answered. The model the request names is not used.
- It waits for the status check or the visitor's approval to end. A site that is not connected gets a 401 in the protocol's error shape, with "AI Wallet is not connected on this site" or "AI Wallet is not installed in this browser". Called outside a browser, it answers 401 with "AI Wallet answers in the browser only".
- An answer that fails before any text comes back as an HTTP error with the wallet's status, so each SDK raises its own typed error. Aborting the SDK's signal stops the chat in the wallet.

The SDKs that take a `fetch` of their own receive it directly:

```ts
import { createOpenAI } from '@ai-sdk/openai';
import { GoogleGenAI } from '@google/genai';

const openai = createOpenAI({ apiKey: client.sdkOptions.apiKey, fetch: client.fetch });
const google = new GoogleGenAI({ apiKey: client.sdkOptions.apiKey, httpOptions: { fetch: client.fetch } });
```

The [existing SDKs guide](/docs/guides/existing-sdks) covers each SDK in full.

## WalletRequestError [#walletrequesterror]

A request the wallet refused or could not answer. It extends `Error` with the request's HTTP `status`.

### WalletRequestError

| Prop      | Type                  | Description |
| --------- | --------------------- | ----------- |
| `cause?`  | `unknown`             |             |
| `status`  | `number`              |             |
| `name`    | `string`              |             |
| `message` | `string`              |             |
| `stack?`  | `string \| undefined` |             |

| Status | Cause |
| --- | --- |
| The provider's status | The provider refused the call. |
| `401` | The visitor has not connected the site. |
| `403` | The visitor declined the connection, the wallet is locked, or it holds no account. |
| `499` | The visitor stopped the answer in AI Wallet. |

AI Wallet builds its errors with its own copy of the class, so a site reads `name` and `status` and leaves `instanceof` aside. A chat aborted by the site's own signal rejects with the signal's reason instead.

```ts
try {
  transcript.push(...await runConversation({ session: state.session, tools, messages: transcript }));
} catch (error) {
  const { name, status } = error as { name?: string; status?: number };
  if (name === 'WalletRequestError' && status === 499) {
    showNotice('You stopped the answer in AI Wallet.');
  }
}
```

## McpToolSource [#mcptoolsource]

The site's tools, read from its MCP server over Streamable HTTP. It implements [`ToolSource`](#tool-types).

### McpToolSourceMembers

| Prop        | Type                                                               | Description |
| ----------- | ------------------------------------------------------------------ | ----------- |
| `listTools` | `() => Promise<ToolDefinition[]>`                                  |             |
| `callTool`  | `(name: string, args: Record<string, unknown>) => Promise<string>` |             |

- **constructor.** `new McpToolSource(url, clientName)` takes the server's address, as a string or a `URL`. A relative URL resolves against the page's address, so outside a page pass an absolute one. `clientName` is the client name your MCP server sees when the client initializes, `aiconnect` by default.
- **listTools().** Connects on first use, reads the tool list once and keeps it for the life of the instance. A failed attempt is forgotten, so the next call asks the server again.
- **callTool(name, args).** Returns the result's text parts joined with line breaks. A result marked `isError` comes back as `Tool error: ` followed by its text.

```ts
const tools = new McpToolSource('/api/mcp', 'Bookshop');

const definitions = await tools.listTools();
const result = await tools.callTool('search_books', { mood: 'calm' });
```

The [MCP guide](/docs/guides/mcp-tools) covers the server side.

## runConversation [#runconversation]

Runs the agent loop in the browser: the model answers or asks for tools, the site's tools run, their results go back to the model, until it answers in plain text.

### RunConversation

| Prop              | Type                                                       | Description |
| ----------------- | ---------------------------------------------------------- | ----------- |
| `runConversation` | `(options: ConversationOptions) => Promise<ChatMessage[]>` |             |

It returns the messages the conversation gained: the assistant turns and the tool results, ending with the final assistant answer. It leaves `options.messages` untouched, so append the result to your transcript yourself.

- It lists the tools once, then sends `messages` and the tool definitions to `session.chat`. It sends no [generation options](#generationoptions); call `session.chat` directly to set them.
- It runs the tools the model asks for one after the other, in the model's order, and adds each result as a `tool` message.
- A tool that throws becomes a result with the text `Tool error: ` and the error's message, which the model reads.
- It stops when an answer has no tool calls, when no `tools` were given, or after `maxSteps` steps (6 by default). The last step offers no tools, so the model has to conclude.
- An error from `session.chat`, such as a provider refusal or the visitor stopping the answer in the wallet, rejects the promise.

```ts
const { state } = client;
if (state.status === 'connected') {
  const asked: ChatMessage = { role: 'user', content: 'A calm book for tonight' };
  const added = await runConversation({
    session: state.session,
    tools,
    messages: [{ role: 'system', content: SYSTEM }, asked],
    onText: (delta) => output.append(delta),
  });
  transcript.push(asked, ...added);
}
```

## ConversationOptions [#conversationoptions]

### ConversationOptions

| Prop            | Type                                                      | Description |
| --------------- | --------------------------------------------------------- | ----------- |
| `session`       | `ProviderSession`                                         |             |
| `messages`      | `ChatMessage[]`                                           |             |
| `tools?`        | `ToolSource \| undefined`                                 |             |
| `maxSteps?`     | `number \| undefined`                                     |             |
| `signal?`       | `AbortSignal \| undefined`                                |             |
| `onText?`       | `((delta: string) => void) \| undefined`                  |             |
| `onToolCall?`   | `((call: ToolCall) => void) \| undefined`                 |             |
| `onToolResult?` | `((call: ToolCall, result: string) => void) \| undefined` |             |

- **session.** The `session` of a `connected` state.
- **messages.** The conversation so far, system prompt first.
- **tools.** The tool source. Without it, the model answers once.
- **maxSteps.** The number of model calls at most. The default is 6.
- **signal.** Passed to `session.chat` on each step. With the wallet's session, aborting it stops the chat in AI Wallet, and the promise rejects with the signal's reason.
- **onText**, **onToolCall**, **onToolResult.** The [hooks](#conversationhooks).

```ts
const controller = new AbortController();
stopButton.onclick = () => controller.abort();

const added = await runConversation({ session: state.session, tools, messages, maxSteps: 3, signal: controller.signal });
```

## ConversationHooks [#conversationhooks]

### ConversationHooks

| Prop            | Type                                                      | Description |
| --------------- | --------------------------------------------------------- | ----------- |
| `onText?`       | `((delta: string) => void) \| undefined`                  |             |
| `onToolCall?`   | `((call: ToolCall) => void) \| undefined`                 |             |
| `onToolResult?` | `((call: ToolCall, result: string) => void) \| undefined` |             |

- **onText.** Each piece of the answer's text as it streams, across every step of the loop.
- **onToolCall.** A tool call, before the tool runs.
- **onToolResult.** The same call with its result, after the tool ran or failed.

```ts
await runConversation({
  session: state.session,
  tools,
  messages,
  onToolCall: (call) => showStatus(`Running ${call.name}`),
  onToolResult: (call, result) => renderCards(call.name, result),
});
```

## finalAnswer [#finalanswer]

### FinalAnswer

| Prop          | Type                                                    | Description |
| ------------- | ------------------------------------------------------- | ----------- |
| `finalAnswer` | `(messages: ChatMessage[]) => AssistantMessage \| null` |             |

Returns the last message when it is an assistant message, and `null` otherwise.

```ts
const added = await runConversation({ session: state.session, tools, messages });
const answer = finalAnswer(added)?.content ?? '';
```

## Chat content helpers [#chat-content-helpers]

The functions that read a message's content, as parts or as text, and an image's `data:` URL as its media type and base64 data. AI Wallet's connectors use them in every translation.

### ChatContentHelpers

| Prop           | Type                                                  | Description |
| -------------- | ----------------------------------------------------- | ----------- |
| `contentParts` | `(content: string \| ContentPart[]) => ContentPart[]` |             |
| `textOf`       | `(content: string \| ContentPart[]) => string`        |             |
| `inlineData`   | `(url: string) => InlineData \| null`                 |             |
| `dataUrl`      | `({ mediaType, data }: InlineData) => string`         |             |

- **contentParts(content).** The content as parts. A string becomes one text part, and an empty string none.
- **textOf(content).** The text of the parts joined, without the images.
- **inlineData(url).** The media type and data of a base64 `data:` URL, or `null` for any other URL.
- **dataUrl(data).** The `data:` URL of an `InlineData`.

### InlineData [#inlinedata]

### InlineData

| Prop        | Type     | Description |
| ----------- | -------- | ----------- |
| `mediaType` | `string` |             |
| `data`      | `string` |             |

```ts
const question: UserMessage = {
  role: 'user',
  content: [
    { type: 'text', text: 'What does this label say?' },
    { type: 'image', url: dataUrl({ mediaType: 'image/png', data: PNG_BASE64 }) },
  ],
};

textOf(question.content); // 'What does this label say?'

// An Anthropic image block: inline data when the URL carries it, the URL otherwise.
const blocks = contentParts(question.content).map((part) => {
  if (part.type === 'text') {
    return { type: 'text', text: part.text };
  }
  const inline = inlineData(part.url);
  return inline
    ? { type: 'image', source: { type: 'base64', media_type: inline.mediaType, data: inline.data } }
    : { type: 'image', source: { type: 'url', url: part.url } };
});
```

## Chat types [#chat-types]

The chat format the site and the wallet share. The wallet translates it to each provider's own format and back. `ChatMessage` is a union of four messages, one per `role`:

### ChatMessagesByRole

| Prop        | Type               | Description |
| ----------- | ------------------ | ----------- |
| `assistant` | `AssistantMessage` |             |
| `system`    | `SystemMessage`    |             |
| `tool`      | `ToolMessage`      |             |
| `user`      | `UserMessage`      |             |

### SystemMessage [#systemmessage]

### SystemMessage

| Prop      | Type       | Description |
| --------- | ---------- | ----------- |
| `role`    | `"system"` |             |
| `content` | `string`   |             |

### UserMessage [#usermessage]

### UserMessage

| Prop      | Type                      | Description |
| --------- | ------------------------- | ----------- |
| `role`    | `"user"`                  |             |
| `content` | `string \| ContentPart[]` |             |

`content` is a string, or a list of parts when the message carries images.

### AssistantMessage [#assistantmessage]

### AssistantMessage

| Prop         | Type                      | Description |
| ------------ | ------------------------- | ----------- |
| `role`       | `"assistant"`             |             |
| `content`    | `string`                  |             |
| `toolCalls?` | `ToolCall[] \| undefined` |             |

### ToolMessage [#toolmessage]

### ToolMessage

| Prop         | Type     | Description |
| ------------ | -------- | ----------- |
| `role`       | `"tool"` |             |
| `toolCallId` | `string` |             |
| `name`       | `string` |             |
| `content`    | `string` |             |

A `ToolMessage` answers the `ToolCall` whose `id` it carries in `toolCallId`.

### ToolCall [#toolcall]

### ToolCall

| Prop        | Type                      | Description |
| ----------- | ------------------------- | ----------- |
| `id`        | `string`                  |             |
| `name`      | `string`                  |             |
| `arguments` | `Record<string, unknown>` |             |

### ContentPart [#contentpart]

A union of two parts, one per `type`:

### ContentPartsByType

| Prop    | Type        | Description |
| ------- | ----------- | ----------- |
| `image` | `ImagePart` |             |
| `text`  | `TextPart`  |             |

### TextPart [#textpart]

### TextPart

| Prop   | Type     | Description |
| ------ | -------- | ----------- |
| `type` | `"text"` |             |
| `text` | `string` |             |

### ImagePart [#imagepart]

### ImagePart

| Prop   | Type      | Description |
| ------ | --------- | ----------- |
| `type` | `"image"` |             |
| `url`  | `string`  |             |

`url` is a `data:` URL, or an https URL for the providers that fetch images themselves.

```ts
const messages: ChatMessage[] = [
  { role: 'system', content: 'You recommend books from this site\'s catalog.' },
  { role: 'user', content: 'A calm book for tonight' },
  { role: 'assistant', content: '', toolCalls: [{ id: 'call_1', name: 'search_books', arguments: { mood: 'calm' } }] },
  { role: 'tool', toolCallId: 'call_1', name: 'search_books', content: '[{"title":"Piranesi"}]' },
];

const cover: ContentPart[] = [
  { type: 'text', text: 'Which book has this cover?' },
  { type: 'image', url: 'https://example.com/covers/piranesi.jpg' },
];
messages.push({ role: 'user', content: cover });
```

## Tool types [#tool-types]

The tools a site offers. Any object that implements `ToolSource` can feed `runConversation` and `useAiChat`.

### ToolDefinition [#tooldefinition]

### ToolDefinition

| Prop          | Type         | Description |
| ------------- | ------------ | ----------- |
| `name`        | `string`     |             |
| `description` | `string`     |             |
| `inputSchema` | `JsonSchema` |             |

### ToolSource [#toolsource]

### ToolSource

| Prop        | Type                                                               | Description |
| ----------- | ------------------------------------------------------------------ | ----------- |
| `listTools` | `() => Promise<ToolDefinition[]>`                                  |             |
| `callTool`  | `(name: string, args: Record<string, unknown>) => Promise<string>` |             |

### JsonSchema [#jsonschema]

The type of `inputSchema`, a JSON Schema object:

### JsonSchemaShape

| Prop         | Type                        | Description |
| ------------ | --------------------------- | ----------- |
| `JsonSchema` | `{ [x: string]: unknown; }` |             |

```ts
const empty: JsonSchema = { type: 'object', properties: {} };

const clock: ToolSource = {
  listTools: async () => [{
    name: 'current_time',
    description: 'The current time of the visitor\'s computer, in UTC, as an ISO 8601 string.',
    inputSchema: empty,
  }],
  callTool: async () => new Date().toISOString(),
};
```

## GenerationOptions [#generationoptions]

What the site asks of the answer, in `ChatRequest.options`.

### GenerationOptions

| Prop              | Type                          | Description |
| ----------------- | ----------------------------- | ----------- |
| `maxTokens?`      | `number \| undefined`         |             |
| `temperature?`    | `number \| undefined`         |             |
| `topP?`           | `number \| undefined`         |             |
| `stop?`           | `string[] \| undefined`       |             |
| `toolChoice?`     | `ToolChoice \| undefined`     |             |
| `responseFormat?` | `ResponseFormat \| undefined` |             |

`maxTokens`, `temperature`, `topP` and `stop` are hints: the account's connector leaves out those its provider refuses. `toolChoice` and `responseFormat` always reach the provider.

### GenerationHint [#generationhint]

The names of the hints, each with the type of its option:

### GenerationHints

| Prop          | Type       | Description |
| ------------- | ---------- | ----------- |
| `maxTokens`   | `number`   |             |
| `stop`        | `string[]` |             |
| `temperature` | `number`   |             |
| `topP`        | `number`   |             |

### ToolChoice [#toolchoice]

Whether the model may answer in text, must call a tool, or must call the named one. A tool choice is one of these strings:

### ToolChoiceStrings

| Prop       | Type         | Description |
| ---------- | ------------ | ----------- |
| `auto`     | `"auto"`     |             |
| `none`     | `"none"`     |             |
| `required` | `"required"` |             |

Or an object that names the tool:

### NamedToolChoice

| Prop   | Type     | Description |
| ------ | -------- | ----------- |
| `name` | `string` |             |

### ResponseFormat [#responseformat]

An answer in JSON, held to `schema` when one is given. `strict` asks the providers that offer it to refuse any schema they cannot enforce exactly.

### ResponseFormat

| Prop      | Type                      | Description |
| --------- | ------------------------- | ----------- |
| `type`    | `"json"`                  |             |
| `schema?` | `JsonSchema \| undefined` |             |
| `name?`   | `string \| undefined`     |             |
| `strict?` | `boolean \| undefined`    |             |

```ts
const { message, finishReason } = await state.session.chat({
  messages: [{ role: 'user', content: 'Three calm books, as JSON.' }],
  options: {
    maxTokens: 400,
    toolChoice: 'none',
    responseFormat: {
      type: 'json',
      name: 'books',
      schema: { type: 'object', properties: { titles: { type: 'array', items: { type: 'string' } } } },
    },
  },
});
const { titles } = JSON.parse(message.content) as { titles: string[] };

// The hints a ChatGPT plan account refuses, which its connector leaves out of every call.
const refusedByChatGpt: GenerationHint[] = ['maxTokens', 'temperature', 'topP'];
```

## Session types [#session-types]

A `ProviderSession` answers chats. On a site it is the wallet answering with the visitor's account: the `session` of a `connected` state, with `providerId` set to `aiwallet` and `model` set to the account's default model, or an empty string with Auto, where the request's `onStart` names the account and model of each answer. Its `chat` sends one request to the wallet and resolves with the answer.

### ProviderSession [#providersession]

### ProviderSession

| Prop         | Type                                              | Description |
| ------------ | ------------------------------------------------- | ----------- |
| `providerId` | `string`                                          |             |
| `model`      | `string`                                          |             |
| `chat`       | `(request: ChatRequest) => Promise<ChatResponse>` |             |

### ChatRequest [#chatrequest]

### ChatRequest

| Prop       | Type                                        | Description |
| ---------- | ------------------------------------------- | ----------- |
| `messages` | `ChatMessage[]`                             |             |
| `tools?`   | `ToolDefinition[] \| undefined`             |             |
| `options?` | `GenerationOptions \| undefined`            |             |
| `signal?`  | `AbortSignal \| undefined`                  |             |
| `onStart?` | `((start: ChatStart) => void) \| undefined` |             |
| `onText?`  | `((delta: string) => void) \| undefined`    |             |

`onStart` receives who answers, once, before the first text or the result: the account's display name and the model the wallet asks it for. A site on Auto learns there which account the wallet picked. `onText` receives the answer's text as it streams. Aborting `signal` stops the chat in the wallet.

### ChatStart [#chatstart]

### ChatStart

| Prop           | Type     | Description |
| -------------- | -------- | ----------- |
| `providerName` | `string` |             |
| `model`        | `string` |             |

### ChatResponse [#chatresponse]

### ChatResponse

| Prop           | Type                 | Description |
| -------------- | -------------------- | ----------- |
| `message`      | `AssistantMessage`   |             |
| `finishReason` | `FinishReason`       |             |
| `model`        | `string`             |             |
| `usage?`       | `Usage \| undefined` |             |

`model` is the model that answered, as the provider named it. `usage` is absent when the provider reports no token counts.

### FinishReason [#finishreason]

Why the answer ended: it was complete, it reached the token limit, it calls tools, or the provider's filter stopped it.

### FinishReasons

| Prop            | Type              | Description |
| --------------- | ----------------- | ----------- |
| `contentFilter` | `"contentFilter"` |             |
| `length`        | `"length"`        |             |
| `stop`          | `"stop"`          |             |
| `toolCalls`     | `"toolCalls"`     |             |

### Usage [#usage]

The tokens the provider counted for the call. `inputTokens` includes `cachedInputTokens`.

### Usage

| Prop                | Type     | Description |
| ------------------- | -------- | ----------- |
| `inputTokens`       | `number` |             |
| `outputTokens`      | `number` |             |
| `cachedInputTokens` | `number` |             |

```ts
const { state } = client;
if (state.status === 'connected') {
  const response: ChatResponse = await state.session.chat({
    messages: [{ role: 'user', content: 'Summarise this page in two sentences.' }],
    onText: (delta) => output.append(delta),
  });
  if (response.finishReason === 'length') {
    output.append(' (cut at the token limit)');
  }
  const usage: Usage | undefined = response.usage;
  console.log(response.model, usage?.inputTokens, usage?.outputTokens);
}
```

## WalletLink [#walletlink]

What a site sees of the wallet: whether it may use the visitor's AI, and then which account answers it and on which model. `AiConnect` builds its `ConnectionState` from the links the wallet returns and announces. You meet this type directly when you implement the [wallet protocol](/docs/protocol/wallet-provider).

A site that is not connected:

### WalletUnlinked

| Prop        | Type    | Description |
| ----------- | ------- | ----------- |
| `connected` | `false` |             |

A connected site:

### WalletLinked

| Prop           | Type                  | Description |
| -------------- | --------------------- | ----------- |
| `connected`    | `true`                |             |
| `providerName` | `string`              |             |
| `model?`       | `string \| undefined` |             |

```ts
function describe(link: WalletLink): string {
  return link.connected ? `${link.providerName} answers this site` : 'Not connected';
}
```

## WalletProvider [#walletprovider]

The wallet as a page sees it: `window.aiwallet`, which the extension announces to every page. The [wallet protocol](/docs/protocol/wallet-provider) specifies its methods.

### WalletProvider

| Prop         | Type                                                   | Description |
| ------------ | ------------------------------------------------------ | ----------- |
| `isAIWallet` | `true`                                                 |             |
| `request`    | `function`                                             |             |
| `onLink`     | `(listener: (link: WalletLink) => void) => () => void` |             |

A chat streams its events to `onEvent`: one `start` naming who answers, then each piece of text. Aborting `signal` rejects the request with the signal's reason, and the wallet stops the chat it runs for it. `onLink` registers a listener for the visitor's changes, in the wallet, to the account or model the site answers with, and returns a function that removes it.

### WalletChatParams [#walletchatparams]

What a site sends with a `chat` request.

### WalletChatParams

| Prop       | Type                             | Description |
| ---------- | -------------------------------- | ----------- |
| `messages` | `ChatMessage[]`                  |             |
| `tools?`   | `ToolDefinition[] \| undefined`  |             |
| `options?` | `GenerationOptions \| undefined` |             |

A wallet of your own for a page's tests, set before the page creates its `AiConnect`:

```ts
const testWallet: WalletProvider = {
  isAIWallet: true,
  request: async (method, params, onEvent) => {
    if (method === 'status' || method === 'connect') {
      return { connected: true, providerName: 'Test' } satisfies WalletLink;
    }
    if (method === 'disconnect') {
      return { connected: false } satisfies WalletLink;
    }
    if (method === 'chat') {
      const { messages } = params as WalletChatParams;
      const content = `You said: ${textOf(messages.at(-1)?.content ?? '')}`;
      onEvent?.({ kind: 'start', providerName: 'Test', model: 'test' });
      onEvent?.({ kind: 'text', delta: content });
      return { message: { role: 'assistant', content }, finishReason: 'stop', model: 'test' } satisfies ChatResponse;
    }
    throw new WalletRequestError(`Unknown method ${method}`, 400);
  },
  onLink: () => () => undefined,
};

Object.assign(window, { aiwallet: testWallet });
```

### ChatEvent [#chatevent]

What a chat streams to the page before its result, in order: one `start`, sent before the first text or, when the answer has no text, before the result, then a `text` event per piece of text. The [wallet protocol](/docs/protocol/wallet-provider#streaming-events) describes them.

### ChatEvents

| Prop    | Type                               | Description |
| ------- | ---------------------------------- | ----------- |
| `start` | `{ kind: 'start'; } & ChatStart`   |             |
| `text`  | `{ kind: 'text'; delta: string; }` |             |
