Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .changeset/anthropic-drop-model-options-system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@tanstack/ai-anthropic': minor
---

Drop `modelOptions.system` from the accepted Anthropic provider options. System prompts must now flow through the top-level `systemPrompts` option on `chat()`/`structuredOutput()`.

This was an undocumented escape hatch only reachable via casts, but it was the only way to attach `cache_control` to system blocks for prompt caching. Until `systemPrompts` is widened to carry per-prompt metadata (planned follow-up), callers who need Anthropic `cache_control` on system text should keep using `modelOptions.system` on the prior version, or supply a custom adapter override.

To make the removal loud instead of silent, the adapter now logs an `errors`-category log line via the provided `Logger` whenever unknown `modelOptions` keys are passed (including `system`). Callers casting around the public type will see the dropped keys named in the log, rather than the request going out without them.
11 changes: 10 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@
"updateInternalDependencies": "patch",
"fixed": [],
"linked": [],
"ignore": [],
"ignore": [
"@tanstack/ai-codemods",
"@tanstack/ai-code-mode-models-eval",
"@tanstack/ai-e2e",
"stream-processor-panel",
"php-slim",
"python-fastapi",
"ts-*",
"vanilla-chat"
],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
Expand Down
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab
11 changes: 11 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"vitest.explorer",
"nrwl.angular-console",
"svelte.svelte-vscode",
"Vue.volar",
"EditorConfig.EditorConfig"
]
}
161 changes: 161 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Contributing to TanStack AI

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

## Prerequisites

- **pnpm**: 10.17.0 or newer. Use the version pinned in `packageManager` (`pnpm@11.1.1`).
- Recommended: install via [Corepack](https://nodejs.org/api/corepack.html). Run `corepack enable` once and pnpm is managed automatically.
- **Git**.

## Initial setup

```bash
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/typescript/ # 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, PHP, Python, vanilla)
codemods/ # Internal codemods (not published)
docs/ # Documentation source
scripts/ # Repo-level scripts (doc generation, model sync, link verification)
```

- Direct children of `packages/typescript/` 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.

## 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:eslint` |
| 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/typescript/<pkg>` and use its scripts directly (`pnpm test:lib`, `pnpm test:types`, etc.).

## 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:

```jsonc
{
"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/typescript/<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 ESLint.

## 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`.

## Changesets

Any change that ships in a published package requires a changeset. Examples, internal test harnesses, codemods, and docs do not.

```bash
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.

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. CI runs: `pnpm test:pr` (sherif workspace check, knip dead-code, docs link verification, ESLint, unit tests, typecheck, build artifacts, build) + the full E2E suite.
3. Address review comments.
4. 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.

## Adding a new provider adapter

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

1. Create `packages/typescript/ai-<provider>/` with `package.json`, `tsconfig.json`, `src/`, `tests/`, `README.md`. Copy structure from an existing adapter.
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. Add a `pnpm changeset` entry.

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: open a GitHub issue with a minimal repro (the bug report template in `.github/issue_template/bug_report.yml` walks you through it).
- Questions / discussions: [TanStack Discord](https://tlinz.com/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](./CODE_OF_CONDUCT.md).
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ A powerful, type-safe AI SDK for building AI-powered applications.

### <a href="https://tanstack.com/ai">Read the docs →</a>

## Requirements

- **Node.js v24+** is required to avoid compatibility issues with `isolated-vm`.

## Tree-Shakeable Adapters

Import only the functionality you need for smaller bundle sizes:
Expand Down
2 changes: 1 addition & 1 deletion examples/ts-code-mode-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"typescript": "5.9.3",
"vite": "^7.2.7"
"vite": "^7.3.3"
}
}
2 changes: 1 addition & 1 deletion examples/ts-group-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"@vitejs/plugin-react": "^5.1.2",
"jsdom": "^27.2.0",
"typescript": "5.9.3",
"vite": "^7.2.7",
"vite": "^7.3.3",
"vitest": "^4.0.14",
"web-vitals": "^5.1.0"
}
Expand Down
2 changes: 1 addition & 1 deletion examples/ts-react-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"@vitejs/plugin-react": "^5.1.2",
"jsdom": "^27.2.0",
"typescript": "5.9.3",
"vite": "^7.2.7",
"vite": "^7.3.3",
"vitest": "^4.0.14",
"web-vitals": "^5.1.0"
}
Expand Down
2 changes: 1 addition & 1 deletion examples/ts-react-media/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"typescript": "5.9.3",
"vite": "^7.2.7"
"vite": "^7.3.3"
}
}
2 changes: 1 addition & 1 deletion examples/ts-react-search/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"@vitejs/plugin-react": "^5.1.2",
"jsdom": "^27.2.0",
"typescript": "5.9.3",
"vite": "^7.2.7",
"vite": "^7.3.3",
"vitest": "^4.0.14",
"web-vitals": "^5.1.0"
}
Expand Down
2 changes: 1 addition & 1 deletion examples/ts-solid-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"@types/node": "^24.10.1",
"jsdom": "^27.2.0",
"typescript": "5.9.3",
"vite": "^7.2.7",
"vite": "^7.3.3",
"vite-plugin-solid": "^2.11.10",
"vitest": "^4.0.14",
"web-vitals": "^5.1.0"
Expand Down
2 changes: 1 addition & 1 deletion examples/ts-svelte-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@
"tailwindcss": "^4.1.18",
"tslib": "^2.8.1",
"typescript": "5.9.3",
"vite": "^7.2.7"
"vite": "^7.3.3"
}
}
2 changes: 1 addition & 1 deletion examples/ts-vue-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"tailwindcss": "^4.1.18",
"tsx": "^4.21.0",
"typescript": "5.9.3",
"vite": "^7.2.7",
"vite": "^7.3.3",
"vue-tsc": "^2.2.10"
}
}
2 changes: 1 addition & 1 deletion examples/vanilla-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
"@tanstack/ai-client": "workspace:*"
},
"devDependencies": {
"vite": "^7.2.7"
"vite": "^7.3.3"
}
}
23 changes: 13 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,29 @@
},
"packageManager": "pnpm@11.1.1",
"type": "module",
"engines": {
"pnpm": ">=10.17.0"
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"scripts": {
"clean": "pnpm --filter \"./packages/**\" run clean",
"clean:all": "git clean -fdx --exclude=\"!.env\"",
"test": "pnpm run test:ci",
"test:pr": "nx affected --targets=test:sherif,test:knip,test:docs,test:eslint,test:lib,test:types,test:build,build",
"test:ci": "nx run-many --targets=test:sherif,test:knip,test:docs,test:eslint,test:lib,test:types,test:build,build",
"test:eslint": "nx affected --target=test:eslint --exclude=examples/**",
"test:pr": "nx affected --targets=test:sherif,test:knip,test:docs,test:eslint,test:lib,test:types,test:build,build --exclude=examples/**,testing/**",
"test:ci": "nx run-many --targets=test:sherif,test:knip,test:docs,test:eslint,test:lib,test:types,test:build,build --exclude=examples/**,testing/**",
"test:eslint": "nx affected --target=test:eslint --exclude=examples/**,testing/**",
"test:sherif": "sherif",
"test:lib": "nx affected --targets=test:lib --exclude=examples/**",
"test:lib": "nx affected --targets=test:lib --exclude=examples/**,testing/**",
"test:lib:dev": "pnpm test:lib && nx watch --all -- pnpm test:lib",
"test:coverage": "nx affected --targets=test:coverage --exclude=examples/**",
"test:build": "nx affected --target=test:build --exclude=examples/**",
"test:types": "nx affected --targets=test:types --exclude=examples/**",
"test:coverage": "nx affected --targets=test:coverage --exclude=examples/**,testing/**",
"test:build": "nx affected --target=test:build --exclude=examples/**,testing/**",
"test:types": "nx affected --targets=test:types --exclude=examples/**,testing/**",
"test:knip": "knip",
"test:docs": "node scripts/verify-links.ts",
"test:e2e": "pnpm --filter @tanstack/ai-e2e test:e2e",
"test:e2e:ui": "pnpm --filter @tanstack/ai-e2e test:e2e:ui",
"codemod:ag-ui-compliance": "pnpm --filter @tanstack/ai-codemods exec node ./run.mjs ag-ui-compliance",
"build": "nx affected --skip-nx-cache --targets=build --exclude=examples/**",
"build:all": "nx run-many --targets=build --exclude=examples/**",
"build": "nx affected --skip-nx-cache --targets=build --exclude=examples/**,testing/**",
"build:all": "nx run-many --targets=build --exclude=examples/**,testing/**",
"watch": "pnpm run build:all && env NX_DAEMON=true nx watch --all -- pnpm run build:all",
"dev": "pnpm run watch",
"dev:chat": "pnpm --filter ts-react-chat dev",
Expand Down Expand Up @@ -70,7 +73,7 @@
"tinyglobby": "^0.2.15",
"tsx": "^4.21.0",
"typescript": "5.9.3",
"vite": "^7.2.7",
"vite": "^7.3.3",
"vitest": "^4.0.14"
}
}
22 changes: 21 additions & 1 deletion packages/typescript/ai-anthropic/src/adapters/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,11 +301,31 @@ export class AnthropicTextAdapter<
'mcp_servers',
'service_tier',
'stop_sequences',
'system',
'thinking',
'tool_choice',
'top_k',
]
const validKeySet = new Set<string>(validKeys)
const droppedKeys = Object.keys(modelOptions).filter(
(key) => !validKeySet.has(key),
)
if (droppedKeys.length > 0) {
// Reachable when callers cast around the public type (e.g.
// `modelOptions: { system: ... } as any`). Without this warning the
// unknown keys are silently dropped — `system` in particular was a
// previously-tested path for attaching `cache_control` and we don't
// want that to fail in production with no signal.
options.logger.errors(
`anthropic.mapCommonOptionsToAnthropic dropped unknown modelOptions key(s): ${droppedKeys.join(', ')}`,
{
source: 'anthropic.mapCommonOptionsToAnthropic',
droppedKeys,
hint: droppedKeys.includes('system')
? 'pass system prompts via the top-level `systemPrompts` option; `modelOptions.system` is no longer honored'
: undefined,
},
)
}
for (const key of validKeys) {
if (key in modelOptions) {
const value = modelOptions[key]
Expand Down
Loading
Loading