# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Repository Overview

TanStack AI is a type-safe, provider-agnostic AI SDK for building AI-powered applications. The repository is a **pnpm monorepo** managed with **Nx** that includes TypeScript packages, plus multiple framework examples.

**Docs skill (mandatory).** Before planning, writing, editing, or reorganizing anything under `docs/`, load `.claude/skills/docs/SKILL.md` and follow it. Do not write docs without it. See **Documentation** below for TanStack-specific rules that also apply.

**PR description skill (mandatory).** Before `gh pr create`, and after an agent `git push` on a branch that already has an open PR, load `.claude/skills/pr-description/SKILL.md` and follow it. Do not invent the title and body from memory.

**Contributing guide (mandatory).** Before you open a GitHub issue or pull request, read `CONTRIBUTING.md` and follow it. Use the issue or PR template. Update `docs/` when the change is user-facing. Add a changeset on the PR when a published package changed.

**Ponytail skill (mandatory).** Before planning, writing, or editing application code, tests, or examples, load `.claude/skills/ponytail/SKILL.md` and follow it. Do not design or implement without it. Ponytail does not skip this repo's quality gates, E2E tests, or the docs and PR-description skills.

## Package Manager & Tooling

- **Package Manager**: pnpm@10.17.0 (required)
- **Build System**: Nx for task orchestration and caching
- **TypeScript**: 7.0.2 (native Go compiler). Framework build/typecheck tools
  that still need the pre-7 JS Compiler API (svelte-package, svelte-check,
  vue-tsc, knip, typedoc) run against the `@typescript/typescript6` (6.0.2)
  shim via `pnpm-workspace.yaml` packageExtensions. Angular cannot run on TS7,
  so it pins `typescript@5.9.3` instead. kiira 0.6.0 is TS7-native and does
  not need a shim. See that file's comments.
- **Testing**: Vitest for unit tests
- **Linting**: oxlint (incl. type-aware rules via `oxlint-tsgolint`); a few
  ESLint-compat rules run through oxlint's JS-plugin layer
  (`oxlint-plugin-eslint`, `eslint-plugin-unused-imports`, `@stylistic`)
- **Formatting**: oxfmt

Run `pnpm install` before starting any task and again after every merge with
`main`.

## Common Commands

### Testing

```bash
# Run all tests (full CI suite)
pnpm test

# Run tests for affected packages only (for PRs)
pnpm test:pr

# Run specific test suites
pnpm test:lib              # Run unit tests for affected packages
pnpm test:lib:dev          # Watch mode for unit tests
pnpm test:oxlint           # Lint affected packages (oxlint, incl. type-aware)
pnpm test:types            # Type check affected packages
pnpm test:build            # Verify build artifacts with publint
pnpm test:coverage         # Generate coverage reports
pnpm test:knip             # Check for unused dependencies
pnpm test:sherif           # Check pnpm workspace consistency
pnpm test:docs             # Verify documentation links

# E2E tests (required for all changes)
pnpm --filter @tanstack/ai-e2e test:e2e    # Run E2E tests
pnpm --filter @tanstack/ai-e2e test:e2e:ui # Run with Playwright UI
```

### Testing Individual Packages

```bash
# Navigate to package directory and run tests
cd packages/ai
pnpm test:lib              # Run tests for this package
pnpm test:lib:dev          # Watch mode
pnpm test:types            # Type check
pnpm test:oxlint           # Lint (oxlint)
```

### Building

```bash
# Build affected packages
pnpm build

# Build all packages
pnpm build:all

# Watch mode (build + watch for changes)
pnpm watch
pnpm dev  # alias for watch
```

### Code Quality

```bash
pnpm format                # Format all files with oxfmt
```

### Changesets (Release Management)

```bash
pnpm changeset             # Create a new changeset
pnpm changeset:version     # Bump versions based on changesets
pnpm changeset:publish     # Publish to npm
```

## Architecture

### Monorepo Structure

```
packages/                # TypeScript packages (main implementation)
├── ai/                  # Core AI library (@tanstack/ai)
├── ai-client/           # Framework-agnostic chat client
├── ai-react/            # React hooks (useChat)
├── ai-solid/            # Solid hooks
├── ai-svelte/           # Svelte integration
├── ai-vue/              # Vue integration
├── ai-openai/           # OpenAI adapter
├── ai-anthropic/        # Anthropic/Claude adapter
├── ai-gemini/           # Google Gemini adapter
├── ai-ollama/           # Ollama adapter
├── ai-devtools/         # DevTools integration
├── react-ai-devtools/   # React DevTools component
└── solid-ai-devtools/   # Solid DevTools component

testing/
├── e2e/                 # E2E tests (Playwright + aimock) — MANDATORY for all changes
└── panel/               # Vendor integration panel

examples/                # Example applications
├── ts-react-chat/       # React chat example
├── ts-solid-chat/       # Solid chat example
├── ts-vue-chat/         # Vue chat example
├── ts-svelte-chat/      # Svelte chat example
├── ts-group-chat/       # Multi-user group chat
└── vanilla-chat/        # Vanilla JS example
```

### Core Architecture Concepts

#### 1. Adapter System (Tree-Shakeable)

The library uses a **tree-shakeable adapter architecture** where each provider (OpenAI, Anthropic, Gemini, Ollama) exports multiple specialized adapters:

- **Text adapters** (`openaiText`, `anthropicText`) - Chat/completion
- **Embedding adapters** (`openaiEmbed`) - Text embeddings
- **Summarize adapters** (`openaiSummarize`) - Summarization
- **Image adapters** (`openaiImage`) - Image generation

Each adapter is a separate import to minimize bundle size:

```typescript
import { openaiText } from '@tanstack/ai-openai/adapters'
import { ai } from '@tanstack/ai'

const textAdapter = openaiText()
const result = ai({ adapter: textAdapter, model: 'gpt-4o', messages: [...] })
```

#### 2. Core Functions

The `@tanstack/ai` package provides core functions:

- **`ai()`** / **`generate()`** - Unified generation function for any adapter type
- **`chat()`** - Chat completion with streaming, tools, and agent loops
- **`embedding()`** - Generate embeddings
- **`summarize()`** - Summarize text
- Legacy adapters (monolithic, deprecated) use `openai()`, `anthropic()`, etc.

#### 3. Isomorphic Tool System

Tools are defined once with `toolDefinition()` and can have `.server()` or `.client()` implementations:

```typescript
const tool = toolDefinition({
  name: 'getTodos',
  inputSchema: z.object({ userId: z.string() }),
  outputSchema: z.array(z.object({ id: z.string(), title: z.string() })),
})

// Server implementation (runs on server)
const serverTool = tool.server(async ({ userId }) => db.todos.find({ userId }))

// Client implementation (runs in browser)
const clientTool = tool.client(async ({ userId }) =>
  fetch(`/api/todos/${userId}`),
)
```

#### 4. Framework Integrations

- **`@tanstack/ai-client`** - Headless chat state management with connection adapters (SSE, HTTP stream, custom)
- **`@tanstack/ai-react`** - `useChat` hook for React
- **`@tanstack/ai-solid`** - `useChat` hook for Solid
- **`@tanstack/ai-vue`** - Vue integration
- **`@tanstack/ai-svelte`** - Svelte integration

Each framework integration uses the headless `ai-client` under the hood.

#### 5. Type Safety Features

- **Per-model type safety** - Provider options are typed based on selected model
- **Multimodal content** - Type-safe image, audio, video, document support based on model capabilities
- **Zod schema inference** - Tools use Zod for runtime validation and type inference
- **`InferChatMessages`** - Type inference for message types based on tools and configuration

### Key Files & Directories

#### Core Package (`packages/ai/src/`)

- **`index.ts`** - Main exports (chat, embedding, summarize, toolDefinition, etc.)
- **`types.ts`** - Core type definitions (ModelMessage, ContentPart, StreamChunk, etc.)
- **`core/`** - Core functions (chat.ts, generate.ts, embedding.ts, summarize.ts)
- **`adapters/`** - Base adapter classes and interfaces
- **`tools/`** - Tool definition system and Zod converter
- **`stream/`** - Stream processing (StreamProcessor, chunking strategies, partial JSON parsing)
- **`utilities/`** - Helpers (message converters, agent loop strategies, SSE utilities)

#### Provider Adapters (e.g., `packages/ai-openai/src/`)

- **`index.ts`** - Exports tree-shakeable adapters (openaiText, openaiEmbed, etc.)
- **`adapters/`** - Individual adapter implementations (text.ts, embed.ts, summarize.ts, image.ts)
- **`model-meta.ts`** - Model metadata for type safety (provider options per model)
- **`openai-adapter.ts`** - Legacy monolithic adapter (deprecated)

## Development Workflow

### Adding a New Feature

1. Create a changeset: `pnpm changeset`
2. Make changes in the appropriate package(s)
3. **Add or update E2E tests** (see E2E Testing below) — this is mandatory for any feature, bug fix, or behavior change
4. Run tests: `pnpm test:lib` (or package-specific tests)
5. Run E2E tests: `pnpm --filter @tanstack/ai-e2e test:e2e`
6. Run type checks: `pnpm test:types`
7. Run linter: `pnpm test:oxlint`
8. Format code: `pnpm format`
9. Verify build: `pnpm test:build` or `pnpm build`

### Pre-PR Quality Gate (MANDATORY)

**Before committing, run the narrowest meaningful quality checks for your changes and confirm they pass locally. Before opening a PR or pushing changes intended for review, run the same checks CI runs.** If you make post-commit changes, rebase, or merge before pushing to a PR, rerun the relevant checks first.

Use the repo-preferred package manager, scripts, and Nx targets where applicable. Do **not** commit or push while quality checks are failing unless the user explicitly instructs otherwise; report the exact failing command and failure instead.

The single canonical command is:

```bash
pnpm test:pr
```

This runs the exact target set the `PR` workflow runs in CI: `nx affected --targets=test:sherif,test:knip,test:docs,test:kiira,test:oxlint,test:lib,test:types,test:build,build`. There is **no** `--exclude=examples/**,testing/**` carve-out — the example apps and `testing/` packages are included, so Nx runs whatever of these targets they define (in practice `build` and `test:types`). Including them means `test:types` is checked at the call sites where the library is actually consumed, catching call-site type regressions that only manifest there (see issue #820). To type-check just the example apps + `testing/` packages locally, run `nx run-many --targets=test:types --projects=examples/**,testing/**`.

If you can't run `test:pr` (e.g. it's too slow on your machine), at minimum run each of these and confirm they're green before pushing:

- `pnpm test:sherif` — workspace consistency
- `pnpm test:knip` — unused dependencies
- `pnpm test:docs` — doc link verification
- `pnpm test:oxlint` — lint (oxlint, incl. type-aware)
- `pnpm test:types` — typecheck (packages)
- `nx run-many --targets=test:types --projects=examples/**,testing/**` — typecheck the example apps + `testing/` packages
- `pnpm test:lib` — unit tests
- `pnpm test:build` — build artifact verification
- `pnpm build` — build all affected packages
- `pnpm --filter @tanstack/ai-e2e test:e2e` — E2E suite (mandatory for any behavior change; see E2E Testing)

Do **not** rely on CI as your first signal. Run locally, fix, then push.

### Working with Examples

Nx type-checks and builds examples as part of `test:pr`/`test:ci` (via their inferred `test:types` and `build` targets). To run an example locally:

```bash
cd examples/ts-react-chat
pnpm install  # if needed
pnpm dev      # start dev server
```

### Nx Workspace

- Uses Nx affected commands to only test/build changed packages
- Nx caching speeds up builds and tests
- `nx.json` configures Nx behavior
- Use `nx run-many` to run commands across multiple packages

## Important Conventions

### Workspace Dependencies

Use the `workspace:` protocol for internal package dependencies in
`package.json`. Which suffix to use depends on whether the field is published:

- **`dependencies`, `peerDependencies`, `optionalDependencies` → `workspace:^`**
  Example: `"@tanstack/ai": "workspace:^"`
  These fields reach consumers. At publish time pnpm rewrites the specifier to
  a real range, so `workspace:^` becomes `^0.43.1` while `workspace:*` becomes
  the exact pin `0.43.1`. An exact pin stops consumers from deduping and makes
  a peer dependency unsatisfiable the moment the internal package releases its
  next patch. Because every package here is still `0.x`, `^0.43.1` resolves to
  `0.43.x` only — it permits patches without allowing a breaking minor.
- **`devDependencies` → `workspace:*`**
  Example: `"@tanstack/ai": "workspace:*"`
  Never published, so the specifier has no effect on consumers, and `*` is the
  correct intent: always build against the local copy.

Private packages (`examples/`, `testing/`) are never published, so `workspace:*`
is fine there in any field.

### Tree-Shakeable Exports

When adding new functionality to provider adapters, create separate adapters rather than adding to monolithic ones. Export from `/adapters` subpath.

### Exports Field

Each package uses `exports` field in package.json for subpath exports (e.g., `@tanstack/ai/adapters`, `@tanstack/ai/event-client`)

### Testing Strategy

- Unit tests in `*.test.ts` files alongside source
- Uses Vitest with happy-dom for DOM testing
- Test coverage via `pnpm test:coverage`
- **E2E tests are mandatory** — see E2E Testing section below

### E2E Testing (REQUIRED)

**Every feature, bug fix, or behavior change MUST include E2E test coverage.** The E2E suite lives at `testing/e2e/` and uses Playwright + [aimock](https://github.com/CopilotKit/aimock) for deterministic LLM mocking.

```bash
# Run all E2E tests
pnpm --filter @tanstack/ai-e2e test:e2e

# Run with Playwright UI (for debugging)
pnpm --filter @tanstack/ai-e2e test:e2e:ui

# Run a specific spec
pnpm --filter @tanstack/ai-e2e test:e2e -- --grep "openai -- chat"

# Record real LLM responses as fixtures
OPENAI_API_KEY=sk-... pnpm --filter @tanstack/ai-e2e record
```

**What to add for your change:**

| Change type                             | What E2E test to add                                                                      |
| --------------------------------------- | ----------------------------------------------------------------------------------------- |
| New provider adapter                    | Add provider to `feature-support.ts` + `test-matrix.ts`. Existing feature tests auto-run. |
| New feature (e.g., new generation type) | Add feature to types, feature config, support matrix. Create fixture + spec file.         |
| Bug fix in chat/streaming               | Add a test case to `chat.spec.ts` or `tools-test/` that reproduces the bug.               |
| Tool system change                      | Add scenario to `tools-test-scenarios.ts` + test in `tools-test/` specs.                  |
| Middleware change                       | Add test to `middleware.spec.ts` with appropriate scenario.                               |
| Client-side change (useChat, etc.)      | Add test covering the observable behavior change.                                         |

**Guide:** See `testing/e2e/README.md` for full instructions on adding tests, recording fixtures, and troubleshooting.

### Documentation

**MANDATORY: load the `docs` skill before touching docs.** Before you
plan, write, edit, or reorganize any file under `docs/`, load the `docs`
skill at `.claude/skills/docs/SKILL.md` (Skill tool, or Read the file).
Do not write docs from memory of this section. If you cannot load the
skill, stop. Tiny copy edits still load the skill; the skill decides
which gates to skip. This also applies when planning a feature (include
a doc-impact list) and when finishing a behavior change (docs must
update before the work is done).

Then also obey these TanStack-specific rules:

- Docs are in `docs/` directory (Markdown)
- Auto-generated docs via `pnpm generate-docs` (TypeDoc)
- Link verification via `pnpm test:docs`
- **No `as` type-assertion casts in doc code samples.** Examples must
  type-check without `as SomeType`. To use a value typed `unknown` (a raw
  JSON Schema tool input, `request.json()`, `JSON.parse`, custom-event
  values, etc.), narrow it with a `typeof` / `in` check or a type guard, or
  validate it with a Standard Schema library — never `as`. (`as const` is
  allowed; it's a const assertion, not a type cast.)
- **Show both sides of the coin.** When a doc spans both server and client,
  include snippets for **both** halves (the server endpoint AND the client
  consumption), not just one.
- **Use the latest model per provider in examples**, sourced from each
  adapter's `model-meta.ts` (the newest `gpt-*`, `claude-*`, `gemini-*`,
  etc.). Don't introduce superseded model ids in new or edited samples.
- **Maintain `addedAt` / `updatedAt` on docs entries in `docs/config.json`.**
  Every page entry carries an `addedAt` (ISO `YYYY-MM-DD`) and, once edited,
  an `updatedAt`. When you touch a docs page, update its entry:
  - **New page** → add the entry with `addedAt` set to today's date.
  - **Content change** to an existing page (new section, new capability,
    reworked guidance, new examples) → set/refresh `updatedAt` to today's
    date.
  - **Bug fixes don't bump anything.** Pure corrections — typos, broken
    links, code-fence languages, formatting, factual fixes — must **not**
    touch `addedAt` or `updatedAt`. Only genuinely new or changed content
    moves these dates.
- **Docs nav: use `"tab"` for reserved words.** The site sorts sidebar entries
  into tabs by keyword-matching `"<sectionLabel> <pageLabel> <to>"` — `overview`,
  `introduction`, `installation`, `quick start`, `tutorial`, `example`,
  `community` — which yanks a page out of its own section. Don't rename around
  it; set `"tab"` on the section or entry in `docs/config.json`
  (`home | get-started | tutorial | guides | api | examples`).

## Key Dependencies

### Core Runtime Dependencies

- `zod` - Schema validation (peer dependency)
- `@alcyone-labs/zod-to-json-schema` - Convert Zod schemas to JSON Schema for LLM tools
- `partial-json` - Parse incomplete JSON from streaming responses

### Provider SDKs (in adapter packages)

- `openai` - OpenAI SDK
- `@anthropic-ai/sdk` - Anthropic SDK
- `@google/generative-ai` - Gemini SDK
- `ollama` - Ollama SDK

### DevTools

- `@tanstack/devtools-event-client` - TanStack DevTools integration
