Skip to content

Latest commit

 

History

History
286 lines (194 loc) · 19.1 KB

CONTRIBUTING.md

File metadata and controls

286 lines (194 loc) · 19.1 KB

Contributing to TanStack AI

Thanks for contributing! This guide covers everything you need to get from a fresh clone to a merged PR.

Before you start

  • Search the open and closed issues and open and closed pull requests before starting work to avoid duplicating an existing report or contribution.
  • Keep each pull request focused on one change or topic. Pull requests that combine unrelated changes will be closed with a request to split them into separately reviewable contributions.
  • Every pull request must follow the TanStack AI pull request template. Write a concise description that clearly explains what changed and why, and complete the template without removing or bypassing its required sections.
  • You may use AI tools to help generate code, but you remain responsible for understanding, testing, and verifying every submitted change. Do not submit unreviewed, low-quality, or irrelevant generated code.
  • Do not mass-submit unrelated or low-quality AI-generated pull requests. We treat that behavior as spam and may close the pull requests, block the contributor, and report the GitHub account.

Prerequisites

  • pnpm: 11.9.0 or newer. Use the version pinned in packageManager (pnpm@11.9.0).
    • Recommended: install via Corepack. Run corepack enable once and pnpm is managed automatically.
  • Node: 24.8.0, the version in .nvmrc. On Node 22, Nx fails to load the workspace plugin.
  • Git.

Initial setup

git clone https://github.com/TanStack/ai.git
cd ai
pnpm install
pnpm run build:all   # build all public packages once so workspace deps resolve

pnpm install runs Playwright's chromium download (used by the E2E suite). If you don't need E2E, you can skip it via PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 pnpm install.

Repository layout

packages/    # Public, published packages (@tanstack/ai, @tanstack/ai-openai, etc.)
testing/                # Internal test harnesses — NOT published
  e2e/                  # Playwright + aimock E2E suite (mandatory coverage for all changes)
  panel/                # Stream processor visualisation panel
examples/               # Example apps (React, Solid, Vue, Svelte, vanilla)
codemods/               # Internal codemods (not published)
docs/                   # Documentation source
scripts/                # Repo-level scripts (doc generation, model sync, link verification, maintainer sweep)
agent-scripts/          # Repo GitHub agents (PR review bot)
  • Direct children of packages/ are public packages (published to npm).
  • Everything under examples/, testing/, and codemods/ is "private": true and excluded from build/publish.
  • The build system is Nx with affected-target detection.
  • The package manager is pnpm with workspace + catalog protocols.

For deeper architecture details (adapter system, isomorphic tools, framework integrations), see CLAUDE.md at the repo root.

Syncing model metadata

pnpm generate:models is the maintainer command behind the daily Sync Model Metadata workflow (branch automated/sync-models). It:

  1. Fetches OpenRouter, Vercel AI Gateway, and Lovable AI Gateway catalogs.
  2. Regenerates packages/ai-openrouter/src/model-meta.ts and the Vercel Gateway model list.
  3. Inserts new native-provider models into packages/ai-openai, ai-anthropic, ai-gemini, and ai-grok.
  4. Inserts new ElevenLabs text-to-speech, music, sound-effect, and voice-design ids from modelschemas into packages/ai-elevenlabs/src/model-meta.ts. Transcription ids stay hand-maintained.
  5. Writes a patch changeset for the packages that changed.

Rules the generator follows:

  • Keep OpenRouter routing aliases (ids that start with ~) in the OpenRouter catalog. Users can pass chat({ model: '~anthropic/claude-haiku-latest' }). The generated constant name maps ~ to _.
  • Do not copy those aliases into native provider files (ai-openai, ai-anthropic, ai-gemini, ai-grok). Those adapters only accept the provider's own ids.
  • For a new native-provider model, write id, modalities, and pricing. Infer features from OpenRouter supported_parameters when that field exists. Do not copy another model's tool list (computer_use, google_search, x_search, and similar).
  • Anthropic first-party ids use dashes (claude-fable-5-1). OpenRouter uses dots (claude-fable-5.1). The generator hyphenates Anthropic ids on insert. Do not copy the dotted OpenRouter slug into ai-anthropic.
  • Write a row in the provider's *ChatModelToolCapabilitiesByName map for every new chat model, even when supports.tools is still []. Missing that row makes ResolveToolCapabilities fall back to readonly [].
  • For new Anthropic models, infer the provider-options mix from the catalog: no sampling parameters → AnthropicMaxTokensOptions; reasoning.mandatory → AnthropicAdaptiveOnlyThinkingOptions plus AnthropicOutputConfigOptions.
  • Leave curated tools and flags on existing models alone. Edit those by hand after the sync PR opens.

Do not rebase or hand-edit automated/sync-models. The next scheduled run force-pushes that branch from main. Merge generator fixes to main first, then let the workflow rebuild the sync PR.

The workflow pushes with GITHUB_TOKEN, so GitHub does not start Test / E2E on that push. After a sync, a maintainer with write access can run the PR checks from the Actions tab, or push an empty commit to automated/sync-models.

Day-to-day commands

All commands are run from the repo root. Nx handles affected detection and caching.

Goal Command
Run unit tests (affected) pnpm test:lib
Watch unit tests pnpm test:lib:dev
Type-check (affected) pnpm test:types
Lint (affected) pnpm test:oxlint
Verify build artifacts pnpm test:build
Format the repo pnpm format
Build (affected) pnpm build
Build everything pnpm build:all
Run the full CI suite locally pnpm test
Run the affected-PR check pnpm test:pr
E2E suite pnpm test:e2e
E2E with Playwright UI pnpm test:e2e:ui

Working on a single package? cd packages/<pkg> and use its scripts directly (pnpm test:lib, pnpm test:types, etc.).

Faster local runs

Root pnpm test, pnpm test:pr, and pnpm test:lib set VITEST_MAX_WORKERS=1. Nx runs 4 tasks at a time (nx.json parallel). Together that stops each package from spawning one Vitest worker per CPU core while 15 packages already run in parallel.

A single package still uses all cores:

cd packages/ai
pnpm test:lib

Local pnpm test:e2e runs openai, anthropic, and gemini. If none of those providers support a feature, the feature still runs on the providers that do. An explicit E2E_PROVIDERS list runs only those providers. CI always runs the full matrix.

E2E_PROVIDERS=* pnpm test:e2e          # full matrix locally
E2E_PROVIDERS=grok pnpm test:e2e       # one provider

Playwright does not retry or record video locally. CI retries twice and keeps the video of a failed test.

Coverage

Coverage runs in CI only. It is not part of pnpm test, pnpm test:pr, or any git hook, and you are not expected to run it locally.

The Coverage job on every PR measures each affected package that has a test:coverage target twice: once on your branch and once on its merge-base with main. The job fails only when a metric (statements, branches, functions, lines) is at or above 60% on the merge-base and under 60% on your branch. Other drops are listed, but they do not fail the job. A package that is already under 60% on main cannot fail it. Packages your PR didn't affect, and packages without that target, are never measured.

There is no baseline file to keep in sync, and nothing to update when a package is added or removed: both numbers come from the same job on the same runner.

Read the numbers in the PR comment that the Coverage job posts. It shows a per-package table with deltas, and the job updates the same comment on each push. A PR from a fork gets no comment, because its token cannot write one. The same table is in the job summary: open the Coverage job from the PR's Checks tab.

Re-measuring the merge-base is usually close to free. On every push to main, a separate Coverage workflow runs test:coverage for every package with the same forwarded args as the PR job, and fills the Nx Cloud cache. test:coverage declares its coverage/ directory as a task output, so the base-side run restores the cached summaries and does not run the tests again. If that workflow did not finish for the merge-base commit (for example, a newer push to main cancelled it), the base-side tests run again. Keep the -- args in coverage.yml and pr.yml identical: Nx hashes them, so any difference causes a cache miss.

If the job says coverage fell under 60%

Add tests covering the code you changed, so each metric is back at 60% or more. That's the whole remedy — there is no number to override. If you genuinely deleted well-tested code and the drop is expected, say so in the PR and a maintainer can merge past the failing check.

Two known limitations:

  • Uncovered .tsx files can't be remapped by the coverage provider and are dropped from the report with a Failed to parse ... Excluding it from coverage warning. .tsx files that tests do load are measured normally, so the UI packages read higher than their real coverage.
  • ai-react-ui, ai-solid-ui, ai-vue-ui, preact-ai-devtools, react-ai-devtools, solid-ai-devtools, and svelte-ai-devtools have no tests, so they have no test:coverage script. The collect step fails when a measured package reports 0 statements. Add the script back when the package gets tests.

A new package that defines test:coverage shows as new and cannot fail the comparison. Packages without that script are not measured.

If a package was measured on the base commit and has no summary on this PR, the job fails. A crashing test:coverage suite on either side also fails the job. It is not treated as new.

Generate a React playground

You need a TanStack Start chat app to try a feature. Do not copy examples/ts-react-chat. Run the generator.

  1. From the repo root, run pnpm nx g @tanstack/workspace-plugin:react-app <name>.
  2. Run pnpm install.
  3. Copy examples/<name>/.env.example to examples/<name>/.env.local and add a key.
  4. Run pnpm --filter <name> dev (port 3100).
  5. Change the index route for the feature. If the server must change, change /api/chat.

Do not commit the new example unless you mean to keep it as a lasting example.

TypeScript configuration

There is a single tsconfig.base.json at the repo root with the shared compilerOptions. Every package extends it and overrides only what's unique to that package (e.g. outDir, JSX runtime, framework lib).

The standardised per-package shape is:

{
  "extends": "../../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "dist",
    // + package-specific overrides only
  },
  "include": ["src", "tests"],
  "exclude": ["node_modules", "dist"],
}

Tests are included in typecheck. vite.config.ts / vitest.config.ts are not — they're tooling configs typechecked by the build tools themselves.

Adding a unit test

  • Place tests under packages/<pkg>/tests/ with the suffix .test.ts (or .test.tsx for JSX).
  • Vitest's defaults discover anything matching **/*.{test,spec}.?(c|m)[jt]s?(x) — no per-package config is needed.
  • Tests are typechecked by tsc and linted by oxlint.

Adding E2E test coverage (required)

Every feature, bug fix, or behaviour change MUST have E2E coverage. See testing/e2e/README.md for the full guide. Quick reference:

Change type What to add
New provider adapter Add provider to feature-support.ts + test-matrix.ts. Tests auto-run.
New feature (e.g. new generation type) Add to types, feature config, support matrix, fixture, spec file.
Chat / streaming bug fix Test case in chat.spec.ts or tools-test/.
Tool system change Scenario in tools-test-scenarios.ts + spec.
Middleware change Test in middleware.spec.ts.
Client-side change (useChat etc.) Test covering the observable behavior change.

Run the suite locally with pnpm test:e2e. Record real LLM fixtures with OPENAI_API_KEY=sk-... pnpm --filter @tanstack/ai-e2e record.

Documentation (required when relevant)

If the change is user-facing, update docs/ in the same PR. Do not ship the code now and the docs later.

User-facing means a caller can see or do something new or different:

  • New or changed public API (exports, types, flags, env vars)
  • New or changed behaviour
  • New adapter capability
  • Changed defaults, errors, or documented contracts

Skip docs only when nothing user-facing changed (CI, internal tests, same-behaviour refactors, agent files). Write that reason in the PR body.

Changesets (required on the PR)

Any PR that changes a published package MUST include a changeset file on that PR. Run this before you open the PR:

pnpm changeset

Pick the affected packages and the bump type:

  • patch: bug fix, internal refactor, perf, docs in package, no API change.
  • minor: new public API, new opt-in behaviour, backwards-compatible enhancement.
  • major: breaking change to a published API surface. Coordinate with maintainers first.

Do not add the changeset after review as a follow-up. It belongs in the first push of the PR.

Skip a changeset only when the PR does not change published packages (docs, CI, examples, testing, contributing). Tick the docs/CI/dev-only box on the PR template.

The defensive ignore list in .changeset/config.json blocks accidental publication from examples/testing/codemods even if "private": true is ever dropped.

Branches and commits

  • Branch off main. Name the branch after the change (fix/openai-streaming-eof, feat/anthropic-cache-control).
  • Conventional Commits aren't strictly enforced, but follow the prefixes you see in git log: feat:, fix:, docs:, refactor:, chore:, ci:.
  • Keep commits logical. The repo prefers a few coherent commits over one giant squash.

Pull request flow

  1. Push your branch and open a PR against main.
  2. Fill the PR template. Tick docs and changeset honestly, or say why you skipped them.
  3. CI runs: pnpm test:pr (sherif workspace check, knip dead-code, docs link verification, oxlint, unit tests, typecheck, build artifacts, build), the Coverage regression gate, and the full E2E suite.
  4. Address review comments.
  5. A maintainer merges. Releases are cut via Changesets. Your changeset entry lands in the next release.

The PR template lists the steps. The Test plan section is required — describe how a reviewer can verify your change.

Automated Grok review

A Grok agent reviews non-draft PRs into main automatically when they open or sync. The first lines of the bot's comment say it is automated. It is not a maintainer review.

The bot sets exactly one of these labels:

  • ai-rejected — the change is not useful, or it does not fix the claimed bug.
  • ai-needs-work — the review listed fixes, but they are not on the branch yet (often a fork with maintainer edits off).
  • ai-ready — the bot thinks a maintainer can merge after they Approve.

The bot never GitHub-approves and never merges. The ready-to-merge label still means a human approval plus green CI.

If the bot pushes, it only commits bugs and suggestions the review listed. Roster maintainers can request one run by writing /ai-review. Anyone with write access can use Actions (workflow_dispatch) on main. For a first-time fork contributor, GitHub holds the automatic run. A maintainer writes /ai-review, and the bot approves the waiting checks after a clean scan.

The bot checks the PR before Grok starts. It blocks sensitive automation, agent instructions, symlinks, binary files, new executable files, new dependencies, lockfile changes, and incomplete patches. A blocked PR gets a bot comment with the reasons and no verdict label. Grok runs in a disposable Docker container without the bot's GitHub token. The host validates Grok's patch before it applies or pushes any edit.

Adding a new provider adapter

The pattern lives in packages/ai-openai/, packages/ai-anthropic/, packages/ai-gemini/, etc. New core adapters typically:

  1. Create packages/ai-<provider>/ with package.json, tsconfig.json, src/, tests/, README.md. Copy structure from an existing adapter. The README must start with the TanStack AI <picture> banner from packages/ai/README.md (https://tanstack.com/api/readme/ai.png). Do not use media/header_ai.png.
  2. Implement tree-shakeable adapter exports under src/adapters/ (text.ts, embed.ts, summarize.ts, etc.).
  3. Add model-meta.ts so per-model type safety works.
  4. Wire the provider into testing/e2e/feature-support.ts and testing/e2e/test-matrix.ts. Existing provider-coverage tests pick it up automatically.
  5. Record fixtures (OPENAI_API_KEY=... pnpm --filter @tanstack/ai-e2e record) — or write deterministic ones by hand. No real API keys at test time.
  6. Update docs/ for the adapter, and add a pnpm changeset entry on the same PR.

If you're building a community/third-party adapter that lives outside this repo, follow docs/community-adapters/guide.md instead.

Known gaps

  • Vue/Svelte SFCs are not currently linted. Our linter doesn't yet support .vue/.svelte parsers in the toolchain we use; the script blocks inside those files rely on TypeScript and tests for safety. If you're touching a .svelte or .vue file, lean on tsc / svelte-check / vue-tsc and explicit tests.
  • Build configs (vite.config.ts, vitest.config.ts) are not in the tsc typecheck pass. They're typechecked at build time by vite/vitest themselves. If you make changes there, run pnpm build or pnpm test:lib to surface issues.

Reporting issues / getting help

  • Bugs: first search the open and closed issues, then use the bug report template with a minimal reproduction if the bug has not already been reported.
  • Questions / discussions: TanStack Discord.
  • Security: follow the disclosure process in SECURITY.md (if applicable) or email the maintainers directly.

Code of Conduct

By participating you agree to abide by our Code of Conduct.