Skip to content
Open
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
48 changes: 48 additions & 0 deletions .changeset/ai-octane-port.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
'@tanstack/ai-octane': minor
---

Add `@tanstack/ai-octane` — [Octane](https://github.com/octanejs/octane) bindings for TanStack AI.

This is a port of `@octanejs/tanstack-ai@0.0.11`, which lived in the
octanejs/octane repo as a temporary stopgap. The code moves here essentially
unchanged apart from the rename; the runtime surface is the same.

The package covers the `@tanstack/ai-react` hook surface — `useChat`,
`useRealtimeChat`, `useMcpAppBridge`, `useGeneration`, `useGenerateImage` /
`Audio` / `Speech` / `Video`, `useTranscription`, `useSummarize`,
`useAudioRecorder` — plus the 30 `@tanstack/ai-client` convenience re-exports,
reusing `@tanstack/ai` and `@tanstack/ai-client` unchanged. SSR through
`octane/server` is supported and tested.

Three defects found while reviewing the port were fixed rather than mirrored, and
are covered by tests (each verified to fail if the fix is reverted). Issues are
filed upstream so the React adapter can catch up:

- `useAudioRecorder`'s transforming overload now requires `onComplete`.
Previously, passing any unrelated option (`useAudioRecorder({ onError })`)
matched it, inferred `TOnComplete` as `unknown`, and silently collapsed
`recording`/`stop()` to `unknown`.
- `useGeneration` spreads caller `devtools` metadata before the hardcoded
`framework`/`hookName`, so a caller can no longer misattribute the binding in
the devtools. The sibling hooks already ordered it this way.
- `UseGenerationReturn` is now `<TInput, TOutput>` and types `generate` as
`(input: TInput)` instead of widening to `(input: Record<string, any>)`, so
required and narrow input fields are checked at the call site. This is the one
place the public _type_ surface differs in shape from `@tanstack/ai-react`;
the runtime surface is unchanged.

Two other things to know:

- Like Svelte packages shipping `.svelte`, this one publishes **uncompiled
source**. The hook modules are `.tsrx` and are compiled by the consumer's
Octane plugin, so there is no `dist` and `octane` is a required peer. The
`.tsrx.d.ts` companions are checked declaration emits, so the full generic
surface is preserved for TypeScript consumers.
- It is baselined against `@tanstack/ai-react@0.17.0`, while this repo is at
0.18.1. The interrupts overhaul (#970) and server-persistence / browser-refresh
durability work (#984) are **not** yet reflected in the Octane hooks; the
`./mcp-apps` subpath is intentionally not ported (it renders a React-only
component). See `packages/ai-octane/status.json` for the full scope,
divergence list, and the exact type-surface gap. Catching up to current parity
is follow-up work.
154 changes: 154 additions & 0 deletions packages/ai-octane/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# @tanstack/ai-octane

[TanStack AI](https://tanstack.com/ai) bindings for the
[Octane](https://github.com/octanejs/octane) UI framework.

This package ports the `@tanstack/ai-react` hook surface onto Octane while
reusing `@tanstack/ai` and `@tanstack/ai-client` unchanged. The runtime export
surface matches the React adapter, so migration starts by changing the package
import:

```ts
// before
import { useChat } from '@tanstack/ai-react'

// after
import { useChat } from '@tanstack/ai-octane'
```

The renderer-bearing hook modules are authored as `.tsrx` and compiled by
Octane. Matching `.tsrx.d.ts` companions are checked declaration emits of those
implementations, preserving the complete generic surface for TypeScript
consumers.

Like Svelte packages shipping `.svelte`, this package publishes **uncompiled
source**: your Octane plugin (`octane/compiler/vite`, or the rspack / rspeedy
equivalents) compiles the `.tsrx` modules as part of your build. There is no
`dist`.

## Install

```bash
pnpm add @tanstack/ai-octane @tanstack/ai @tanstack/ai-client octane
```

## Usage

```tsx
import { useState } from 'octane'
import { useChat } from '@tanstack/ai-octane'

export function Chat() @{
const [input, setInput] = useState('')
const chat = useChat({
fetcher: myFetcher,
})

<div>
@for (const message of chat.messages; key message.id) {
<p>
{(message.role +
': ' +
message.parts
.map((part) => (part.type === 'text' ? part.content : ''))
.join('')) as string}
</p>
}
<input
value={input}
onInput={(event) => setInput(event.currentTarget.value)}
/>
<button
onClick={() => {
void chat.sendMessage(input)
setInput('')
}}
>
Send
</button>
</div>
}
```

`useChat` has no input state of its own — hold the text box value in a local
`useState` and pass it to `sendMessage`. Note the `onInput` handler: Octane
drives text controls per keystroke through the native `input` event, not a
synthetic `onChange`.

## API

The adapter includes `useChat`, `useRealtimeChat`, `useMcpAppBridge`,
`useGeneration`, `useGenerateImage`, `useGenerateAudio`, `useGenerateSpeech`,
`useGenerateVideo`, `useTranscription`, `useSummarize`, and
`useAudioRecorder`. It also re-exports all 30 `@tanstack/ai-client`
convenience helpers and types (`fetchServerSentEvents`, `fetchHttpStream`,
`xhrServerSentEvents`, `xhrHttpStream`, `stream`, `rpcStream`,
`createChatClientOptions`, `createMcpAppBridge`, and their associated types)
unchanged, mirroring the `@tanstack/ai-react` index.

Server rendering through `octane/server` is supported. `useChat` renders its
initial message snapshot without browser-only setup.

## Divergences from `@tanstack/ai-react`

- The `./mcp-apps` subpath and its `MCPAppResource` component are not ported:
they render `AppRenderer` from the React-only `@mcp-ui/client`, which has no
Octane equivalent. The framework-agnostic `useMcpAppBridge` hook is ported
and available on the main entry.
- Octane uses native events: text/file/recorder inputs drive updates via
`onInput`; there is no synthetic `onChange` layer.
- Octane has no StrictMode double-invoke and always provides `useId`, so no
random-id fallback is needed.
- The devtools bridge is tagged `framework: 'octane'` (upstream sends
`'react'`), so the devtools identify this binding correctly.
- Realtime reconnects and token refreshes use the latest `getToken` and adapter
supplied to the hook; upstream captures the first render's callbacks.
- The declared realtime `onStatusChange` callback is invoked alongside the
hook's state update; upstream 0.17.0 currently drops the external callback.
- Changing `useChat`'s connection or fetcher updates the active `ChatClient` in
place and preserves conversation state; upstream 0.17.0 captures the initial
transport.

### Fixed here, still present upstream

Three defects were found during review of the port and fixed rather than
mirrored. All are documented in [`status.json`](./status.json) and covered by
tests, and each is tracked upstream so the other adapters can catch up.

- `useAudioRecorder`'s transforming overload requires `onComplete`. Upstream,
passing any unrelated option (`useAudioRecorder({ onError })`) matched the
transforming overload, inferred `TOnComplete` as `unknown`, and silently
collapsed `recording` and `stop()` to `unknown`.
([#1001](https://github.com/TanStack/ai/issues/1001))
- `useGeneration` spreads caller `devtools` metadata _before_ the hardcoded
`framework`/`hookName`, so a caller can't misattribute the binding in the
devtools. `ai-react` spreads it after; `ai-vue` and `ai-solid` already order it
this way. ([#1002](https://github.com/TanStack/ai/issues/1002))
- `UseGenerationReturn<TInput, TOutput>` types `generate` as `(input: TInput)`.
Upstream declares `UseGenerationReturn<TOutput>` and widens `generate` to
`(input: Record<string, any>)`, so narrow or required input fields go
unchecked. **This is the one place the public type surface differs in shape
from `@tanstack/ai-react`** — the runtime surface is unchanged, so the
"change the import" migration still holds.
([#1003](https://github.com/TanStack/ai/issues/1003))

## Status

This binding was developed as `@octanejs/tanstack-ai` in the
[octanejs/octane](https://github.com/octanejs/octane) repo as a temporary
stopgap, and moved here — apart from the rename, the only changes are the three
fixes listed above and the test-helper hardening noted in `status.json`.

It is baselined against `@tanstack/ai-react@0.17.0`. Current scope, divergences,
and verification state are tracked in [`status.json`](./status.json) — including
the upstream changes not yet reflected here.

The port runs TanStack AI's React adapter tests against Octane across all eleven
hooks, with no skipped, todo, or expected-failure cases (except the untestable
auto-resume case noted in `status.json`). An SSR fixture and the upstream
compile-time type tests are also included.

## License

MIT — contains source derived from
[TanStack AI](https://github.com/TanStack/ai) (MIT), adapted for Octane.
73 changes: 73 additions & 0 deletions packages/ai-octane/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"name": "@tanstack/ai-octane",
"version": "0.0.0",
"description": "Octane bindings for TanStack AI streaming chat, structured outputs, and media generation.",
"author": "Dominic Gannaway",
"license": "MIT",
"homepage": "https://tanstack.com/ai",
"repository": {
"type": "git",
"url": "git+https://github.com/TanStack/ai.git",
"directory": "packages/ai-octane"
},
"bugs": {
"url": "https://github.com/TanStack/ai/issues"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"type": "module",
"engines": {
"node": ">=22"
},
"keywords": [
"ai",
"ai-sdk",
"typescript",
"tanstack",
"octane",
"chat",
"streaming",
"tool-calling",
"structured-outputs",
"media-generation"
],
"//": "Like Svelte packages shipping .svelte, this package publishes uncompiled source: the hook modules are .tsrx and are compiled by the consumer's Octane plugin. There is therefore no build/dist and no publint test:build target. The .tsrx.d.ts companions are checked declaration emits, so `tsc` still type-checks the full public surface.",
"main": "./src/index.ts",
"module": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"files": [
"src",
"README.md"
],
"//lint": "The .tsrx.d.ts companions are generated declaration emits of the .tsrx implementations, so they are not hand-formatted and are excluded from lint rather than edited in place (a regeneration would undo any fix).",
"scripts": {
"lint:fix": "oxlint src --type-aware --ignore-pattern '**/*.tsrx.d.ts' --fix",
"test:oxlint": "oxlint src --type-aware --ignore-pattern '**/*.tsrx.d.ts'",
"test:lib": "vitest run",
"test:lib:dev": "pnpm test:lib --watch",
"test:types": "tsc && tsc -p typetests/tsconfig.json"
},
"dependencies": {
"@tanstack/ai-client": "workspace:*"
},
"peerDependencies": {
"@tanstack/ai": "workspace:^",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use workspace:* for the internal peer dependency.

@tanstack/ai is an internal workspace package, but this manifest uses workspace:^. As per coding guidelines, internal package dependencies in package.json must use workspace:*.

Proposed fix
-    "`@tanstack/ai`": "workspace:^",
+    "`@tanstack/ai`": "workspace:*",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@tanstack/ai": "workspace:^",
"`@tanstack/ai`": "workspace:*",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-octane/package.json` at line 59, Update the `@tanstack/ai`
dependency declaration in the package manifest from workspace:^ to workspace:*,
preserving it as an internal workspace dependency.

Source: Coding guidelines

"octane": "^0.1.17"
},
"devDependencies": {
"@octanejs/testing-library": "0.1.14",
"@standard-schema/spec": "^1.1.0",
"@tanstack/ai": "workspace:*",
"@types/node": "^24.10.1",
"@vitest/coverage-v8": "4.0.14",
"happy-dom": "^20.0.10",
"octane": "0.1.17",
"vite": "^8.1.4",
"zod": "^4.2.0"
}
}
89 changes: 89 additions & 0 deletions packages/ai-octane/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
export { useChat } from './use-chat.tsrx'
export { useRealtimeChat } from './use-realtime-chat.tsrx'
export { useMcpAppBridge } from './use-mcp-app-bridge.tsrx'
export type { UseMcpAppBridgeOptions } from './use-mcp-app-bridge.tsrx'
export type {
DeepPartial,
UseChatOptions,
UseChatReturn,
UIMessage,
ChatRequestBody,
} from './types'
export type {
UseRealtimeChatOptions,
UseRealtimeChatReturn,
} from './realtime-types'

export { useGeneration } from './use-generation.tsrx'
export type {
UseGenerationOptions,
UseGenerationReturn,
} from './use-generation.tsrx'
export { useGenerateImage } from './use-generate-image.tsrx'
export type {
UseGenerateImageOptions,
UseGenerateImageReturn,
} from './use-generate-image.tsrx'
export { useGenerateAudio } from './use-generate-audio.tsrx'
export type {
UseGenerateAudioOptions,
UseGenerateAudioReturn,
} from './use-generate-audio.tsrx'
export { useGenerateSpeech } from './use-generate-speech.tsrx'
export type {
UseGenerateSpeechOptions,
UseGenerateSpeechReturn,
} from './use-generate-speech.tsrx'
export { useTranscription } from './use-transcription.tsrx'
export type {
UseTranscriptionOptions,
UseTranscriptionReturn,
} from './use-transcription.tsrx'
export { useSummarize } from './use-summarize.tsrx'
export type {
UseSummarizeOptions,
UseSummarizeReturn,
} from './use-summarize.tsrx'
export { useGenerateVideo } from './use-generate-video.tsrx'
export type {
UseGenerateVideoOptions,
UseGenerateVideoReturn,
} from './use-generate-video.tsrx'
export { useAudioRecorder } from './use-audio-recorder.tsrx'
export type {
UseAudioRecorderOptions,
UseAudioRecorderReturn,
} from './use-audio-recorder.tsrx'

// Re-export from ai-client for convenience (mirror upstream index.ts)
export {
fetchServerSentEvents,
fetchHttpStream,
xhrServerSentEvents,
xhrHttpStream,
stream,
rpcStream,
createChatClientOptions,
createMcpAppBridge,
type McpAppBridge,
type CreateMcpAppBridgeOptions,
type ChatFetcher,
type ChatFetcherInput,
type ChatFetcherOptions,
type ConnectionAdapter,
type ConnectConnectionAdapter,
type SubscribeConnectionAdapter,
type RunAgentInputContext,
type FetchConnectionOptions,
type XhrConnectionOptions,
type InferChatMessages,
type GenerationClientState,
type ImageGenerateInput,
type AudioGenerateInput,
type SpeechGenerateInput,
type TranscriptionGenerateInput,
type SummarizeGenerateInput,
type VideoGenerateInput,
type VideoGenerateResult,
type VideoStatusInfo,
} from '@tanstack/ai-client'
Loading
Loading