-
-
Notifications
You must be signed in to change notification settings - Fork 287
feat(ai-octane): port Octane bindings from @octanejs/tanstack-ai #1000
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jherr
wants to merge
4
commits into
main
Choose a base branch
from
feat/ai-octane
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
ce6d41e
feat(ai-octane): port Octane bindings from @octanejs/tanstack-ai
jherr dea5747
ci: apply automated fixes
autofix-ci[bot] ed3c8ea
fix(ai-octane): fix three defects inherited from the React adapter
jherr 2bf7d95
docs(ai-octane): record why the package ships without E2E coverage
jherr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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:^", | ||
| "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" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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' |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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/aiis an internal workspace package, but this manifest usesworkspace:^. As per coding guidelines, internal package dependencies inpackage.jsonmust useworkspace:*.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Coding guidelines