From 2ce315beb71e225b2f0e3d242aac4267021aa128 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Wed, 21 Oct 2026 12:04:57 +0200 Subject: [PATCH 01/58] feat!: remove deprecated APIs for 1.0 Remove every deprecated export, option, field, and type alias from all packages, and the internal code that only served them. Remove old wire formats, readers for old saved data, shut-down provider models, and the @tanstack/ai-react-ui, ai-solid-ui, and ai-vue-ui packages. --- examples/ag-ui/servers/bash/server.sh | 7 - examples/ag-ui/servers/go/agui.go | 11 +- examples/ag-ui/servers/go/main.go | 2 - examples/ag-ui/servers/php/lib/Agui.php | 8 - examples/ag-ui/servers/python/server.py | 7 - examples/ag-ui/servers/rust/src/agui.rs | 4 - examples/ag-ui/servers/rust/src/main.rs | 12 +- examples/ag-ui/servers/zig/src/agui.zig | 4 - examples/ag-ui/servers/zig/src/main.zig | 9 - examples/ag-ui/src/App.tsx | 14 +- .../sandbox-cloudflare/src/routes/index.tsx | 2 +- examples/sandbox-web/src/routes/index.tsx | 2 +- .../routes/_banking-demo/api.banking-demo.ts | 2 +- .../src/routes/_banking-demo/banking-demo.tsx | 2 +- .../_database-demo/api.database-demo.ts | 2 +- .../routes/_database-demo/database-demo.tsx | 2 +- .../src/routes/_home/api.product-codemode.ts | 2 +- .../src/routes/_home/api.product-regular.ts | 2 +- .../src/routes/_home/index.tsx | 4 +- .../routes/_npm-github-chat/api.codemode.ts | 2 +- .../_npm-github-chat/npm-github-chat.tsx | 2 +- .../src/routes/_reporting/api.reports.ts | 2 +- .../src/routes/_reporting/reporting-agent.tsx | 2 +- .../src/lib/sqlite-persistence.ts | 8 +- .../src/routes/capability-demo.tsx | 2 +- .../src/routes/durable-persistence.tsx | 2 +- .../ts-react-chat/src/routes/image-gen.tsx | 4 - .../src/routes/issue-176-tool-result.tsx | 2 +- .../ts-react-chat/src/routes/mcp-apps.tsx | 2 +- .../ts-react-chat/src/routes/mcp-demo.tsx | 2 +- .../ts-react-chat/src/routes/queueing.tsx | 6 +- .../src/routes/sandboxes-durable.tsx | 2 +- .../ts-react-chat/src/routes/sandboxes.tsx | 2 +- examples/ts-react-chat/src/routes/threads.tsx | 33 +- .../src/routes/typesafe-tools.tsx | 2 +- examples/ts-solid-chat/src/routes/index.tsx | 111 ++--- .../src/lib/components/Messages.svelte | 44 +- .../ts-svelte-chat/src/routes/+page.svelte | 42 +- .../ts-vue-chat/src/components/Messages.vue | 41 -- examples/ts-vue-chat/src/views/ChatView.vue | 58 ++- packages/ai-angular/src/index.ts | 2 - packages/ai-angular/src/inject-chat.ts | 18 +- .../ai-angular/src/inject-web-mcp-tools.ts | 12 - packages/ai-angular/src/types.ts | 14 +- packages/ai-angular/tests/create-ui.test.ts | 1 - packages/ai-angular/tests/inject-chat.test.ts | 19 +- .../tests/inject-web-mcp-tools.test.ts | 7 - .../ai-angular/ui/src/create-chat-hook.ts | 1 - packages/ai-anthropic/src/adapters/text.ts | 66 +-- packages/ai-anthropic/src/index.ts | 2 +- packages/ai-anthropic/src/message-types.ts | 22 - .../src/text/text-provider-options.ts | 22 +- packages/ai-anthropic/src/tools/bash-tool.ts | 3 - .../src/tools/code-execution-tool.ts | 3 - .../src/tools/computer-use-tool.ts | 3 - .../ai-anthropic/src/tools/custom-tool.ts | 3 - packages/ai-anthropic/src/tools/index.ts | 45 +- .../ai-anthropic/src/tools/memory-tool.ts | 3 - .../src/tools/text-editor-tool.ts | 3 - .../ai-anthropic/src/tools/web-fetch-tool.ts | 3 - .../ai-anthropic/src/tools/web-search-tool.ts | 3 - .../tests/anthropic-adapter.test.ts | 3 - .../ai-anthropic/tests/text.skills.test.ts | 2 +- .../src/converse/stream-processor.ts | 24 - .../ai-bedrock/tests/replay-parity.test.ts | 9 +- packages/ai-byteplus/src/adapters/image.ts | 4 +- packages/ai-byteplus/src/adapters/text.ts | 13 +- .../ai-byteplus/src/adapters/transcription.ts | 4 +- packages/ai-byteplus/src/adapters/video.ts | 4 +- packages/ai-byteplus/tests/image.test.ts | 2 - packages/ai-byteplus/tests/text.test.ts | 28 +- .../ai-byteplus/tests/transcription.test.ts | 1 - packages/ai-byteplus/tests/video.test.ts | 2 - packages/ai-client/src/chat-client.ts | 124 +---- packages/ai-client/src/client-persistor.ts | 5 +- packages/ai-client/src/connection-adapters.ts | 90 +--- packages/ai-client/src/devtools.ts | 15 - packages/ai-client/src/events.ts | 17 - packages/ai-client/src/index.ts | 2 - packages/ai-client/src/interrupt-manager.ts | 109 +---- packages/ai-client/src/realtime-client.ts | 2 +- packages/ai-client/src/realtime-types.ts | 6 - packages/ai-client/src/sse-parser.ts | 7 - packages/ai-client/src/types.ts | 27 +- .../tests/chat-client-context.test.ts | 19 +- .../tests/chat-client-interrupts.test.ts | 75 +-- .../chat-client-reasoning-rejoin.test.ts | 11 - .../tests/chat-client-resume.test.ts | 403 ++-------------- .../chat-client-stop-continuation.test.ts | 132 ----- packages/ai-client/tests/chat-client.test.ts | 237 +-------- packages/ai-client/tests/chat-fetcher.test.ts | 2 +- .../ai-client/tests/client-persistor.test.ts | 3 +- .../ai-client/tests/client-snapshots.test.ts | 2 +- .../tests/connection-adapters-cleanup.test.ts | 18 - .../tests/connection-adapters-xhr.test.ts | 65 +-- .../tests/connection-adapters.test.ts | 63 --- packages/ai-client/tests/devtools.test.ts | 143 ------ packages/ai-client/tests/events.test.ts | 19 - .../tests/interrupts-types.test-d.ts | 3 +- .../ai-client/tests/resume-snapshot.test.ts | 26 +- .../ai-client/tests/sse-done-model.test.ts | 61 --- packages/ai-client/tests/test-utils.ts | 95 +--- packages/ai-client/tests/ui-fixtures.ts | 3 - packages/ai-cohere/src/adapters/rerank.ts | 5 +- .../ai-cohere/tests/rerank-adapter.test.ts | 1 - packages/ai-devtools/src/store/ai-context.tsx | 59 --- .../src/devtools-middleware.ts | 13 +- packages/ai-event-client/src/index.ts | 32 -- packages/ai-fal/src/utils/billing.ts | 1 - packages/ai-fal/tests/audio-adapter.test.ts | 1 - packages/ai-fal/tests/billing.test.ts | 2 - packages/ai-fal/tests/image-adapter.test.ts | 1 - packages/ai-fal/tests/speech-adapter.test.ts | 1 - .../tests/transcription-adapter.test.ts | 1 - packages/ai-fal/tests/video-adapter.test.ts | 1 - packages/ai-gemini/src/adapters/image.ts | 33 +- packages/ai-gemini/src/adapters/text.ts | 36 +- .../experimental/text-interactions/adapter.ts | 45 +- .../src/image/image-provider-options.ts | 28 +- packages/ai-gemini/src/index.ts | 1 - packages/ai-gemini/src/model-meta.ts | 128 +---- .../src/tools/code-execution-tool.ts | 3 - .../ai-gemini/src/tools/computer-use-tool.ts | 3 - .../ai-gemini/src/tools/file-search-tool.ts | 3 - .../ai-gemini/src/tools/google-maps-tool.ts | 3 - .../src/tools/google-search-retriveal-tool.ts | 3 - .../ai-gemini/src/tools/google-search-tool.ts | 3 - packages/ai-gemini/src/tools/index.ts | 36 +- .../ai-gemini/src/tools/url-context-tool.ts | 3 - .../ai-gemini/tests/image-adapter.test.ts | 85 +--- .../tests/image-per-model-type-safety.test.ts | 96 +--- .../ai-grok/src/adapters/transcription.ts | 1 - packages/ai-grok/src/adapters/video.ts | 1 - packages/ai-grok/src/model-meta.ts | 12 - packages/ai-grok/tests/audio-adapters.test.ts | 1 - packages/ai-grok/tests/realtime-token.test.ts | 6 +- packages/ai-grok/tests/video-adapter.test.ts | 1 - .../src/chat-ui/create-chat-hook.tsrx | 1 - .../src/chat-ui/create-chat-hook.tsrx.d.ts | 1 - packages/ai-octane/src/create-chat-hook.ts | 1 - packages/ai-octane/src/index.ts | 2 - packages/ai-octane/src/realtime-types.ts | 2 +- packages/ai-octane/src/types.ts | 15 - packages/ai-octane/src/use-chat.tsrx | 22 - packages/ai-octane/src/use-web-mcp-tools.tsrx | 9 - .../ai-octane/src/use-web-mcp-tools.tsrx.d.ts | 7 - .../tests/conformance/exports.test.ts | 2 - .../conformance/use-chat-fetcher.test.ts | 2 +- .../tests/conformance/use-chat.test.ts | 74 +-- .../conformance/use-realtime-chat.test.ts | 3 +- .../conformance/use-web-mcp-tools.test.ts | 7 - .../ai-octane/typetests/use-chat.test-d.tsx | 1 - packages/ai-ollama/src/adapters/text.ts | 43 +- .../ai-openai/src/adapters/transcription.ts | 5 +- .../src/text/text-provider-options.ts | 48 +- .../ai-openai/src/tools/apply-patch-tool.ts | 1 - .../src/tools/code-interpreter-tool.ts | 1 - .../ai-openai/src/tools/computer-use-tool.ts | 1 - packages/ai-openai/src/tools/custom-tool.ts | 1 - .../ai-openai/src/tools/file-search-tool.ts | 1 - packages/ai-openai/src/tools/function-tool.ts | 1 - .../src/tools/image-generation-tool.ts | 1 - packages/ai-openai/src/tools/index.ts | 12 - .../ai-openai/src/tools/local-shell-tool.ts | 1 - packages/ai-openai/src/tools/mcp-tool.ts | 1 - packages/ai-openai/src/tools/shell-tool.ts | 1 - .../src/tools/web-search-preview-tool.ts | 1 - .../ai-openai/src/tools/web-search-tool.ts | 1 - .../tests/transcription-adapter.test.ts | 1 - .../tests/transcription-usage.test.ts | 1 - .../ai-openai/tests/video-adapter.test.ts | 21 - packages/ai-openrouter/src/adapters/rerank.ts | 1 - .../src/adapters/responses-text.ts | 47 -- packages/ai-openrouter/src/adapters/text.ts | 58 --- packages/ai-openrouter/src/index.ts | 6 +- packages/ai-openrouter/src/tools/index.ts | 1 - .../src/tools/web-search-tool.ts | 3 - .../tests/openrouter-adapter.test.ts | 167 +------ .../tests/rerank-adapter.test.ts | 1 - .../ai-openrouter/tests/video-adapter.test.ts | 19 - packages/ai-persistence/src/index.ts | 6 +- packages/ai-persistence/src/middleware.ts | 145 +----- packages/ai-persistence/src/retrieve.ts | 23 +- .../ai-persistence/src/testkit/conformance.ts | 3 +- packages/ai-persistence/src/types.ts | 12 +- .../tests/artifact-thread.test.ts | 1 + .../tests/generation-artifacts.test.ts | 26 +- .../ai-persistence/tests/interrupts.test.ts | 280 ----------- .../tests/with-persistence.test.ts | 12 +- .../src/chat-ui/create-chat-hook.tsx | 1 - packages/ai-preact/src/create-chat-hook.ts | 1 - packages/ai-preact/src/index.ts | 7 +- packages/ai-preact/src/types.ts | 17 +- packages/ai-preact/src/use-chat.ts | 31 +- packages/ai-preact/src/use-web-mcp-tools.ts | 12 - packages/ai-preact/tests/use-chat.test.ts | 74 +-- .../ai-preact/tests/use-web-mcp-tools.test.ts | 12 +- packages/ai-react-ui/CHANGELOG.md | 72 --- packages/ai-react-ui/LICENSE | 21 - packages/ai-react-ui/README.md | 29 -- packages/ai-react-ui/package.json | 57 --- packages/ai-react-ui/src/index.ts | 6 - packages/ai-react-ui/tsconfig.json | 11 - packages/ai-react-ui/vite.config.ts | 24 - packages/ai-react/src/chat-ui/chat-input.tsx | 177 ------- .../ai-react/src/chat-ui/chat-message.tsx | 271 ----------- .../ai-react/src/chat-ui/chat-messages.tsx | 89 ---- packages/ai-react/src/chat-ui/chat.tsx | 98 ---- .../ai-react/src/chat-ui/create-chat-hook.tsx | 1 - .../ai-react/src/chat-ui/tool-approval.tsx | 132 ----- packages/ai-react/src/create-chat-hook.ts | 1 - packages/ai-react/src/index.ts | 7 +- packages/ai-react/src/realtime-types.ts | 2 +- packages/ai-react/src/types.ts | 17 +- packages/ai-react/src/ui.ts | 17 - packages/ai-react/src/use-chat.ts | 22 - packages/ai-react/src/use-web-mcp-tools.ts | 12 - .../tests/chat-ui/coexistence.test.ts | 26 - .../ai-react/tests/use-chat-fetcher.test.ts | 2 +- packages/ai-react/tests/use-chat.test.ts | 80 +-- .../ai-react/tests/use-web-mcp-tools.test.ts | 12 +- packages/ai-remix/src/chat-ui/chat-input.tsx | 125 ----- .../ai-remix/src/chat-ui/chat-message.tsx | 197 -------- .../ai-remix/src/chat-ui/chat-messages.tsx | 67 --- packages/ai-remix/src/chat-ui/chat.tsx | 48 -- .../ai-remix/src/chat-ui/create-chat-hook.ts | 1 - .../ai-remix/src/chat-ui/tool-approval.tsx | 94 ---- packages/ai-remix/src/create-chat.ts | 23 - packages/ai-remix/src/create-web-mcp-tools.ts | 12 - packages/ai-remix/src/index.ts | 2 - packages/ai-remix/src/realtime-types.ts | 2 +- packages/ai-remix/src/types.ts | 16 - packages/ai-remix/src/ui.ts | 20 - .../tests/create-realtime-chat.test.ts | 7 +- .../tests/create-web-mcp-tools.test.ts | 12 +- packages/ai-remix/tests/exports.test.ts | 2 - packages/ai-sandbox-cloudflare/src/agent.ts | 8 +- .../ai-sandbox-cloudflare/src/coordinator.ts | 7 +- .../ai-sandbox-cloudflare/src/run-log-do.ts | 74 +-- packages/ai-sandbox-cloudflare/src/run-log.ts | 93 +--- .../tests/run-log-do.test.ts | 63 --- packages/ai-sandbox/src/index.ts | 1 - .../ai-sandbox/src/memory-snapshot-types.ts | 2 +- packages/ai-sandbox/src/middleware.ts | 4 +- packages/ai-sandbox/src/snapshots.ts | 9 +- packages/ai-sandbox/src/watch.ts | 2 - packages/ai-sandbox/tests/fakes.ts | 1 - packages/ai-sandbox/tests/snapshots.test.ts | 25 +- packages/ai-sandbox/tests/watch.test.ts | 32 +- packages/ai-solid-ui/CHANGELOG.md | 65 --- packages/ai-solid-ui/LICENSE | 21 - packages/ai-solid-ui/README.md | 29 -- packages/ai-solid-ui/package.json | 57 --- packages/ai-solid-ui/src/index.ts | 6 - packages/ai-solid-ui/tsconfig.json | 12 - packages/ai-solid-ui/vite.config.ts | 24 - packages/ai-solid/src/chat-ui/chat-input.tsx | 168 ------- .../ai-solid/src/chat-ui/chat-message.tsx | 261 ---------- .../ai-solid/src/chat-ui/chat-messages.tsx | 91 ---- packages/ai-solid/src/chat-ui/chat.tsx | 95 ---- .../ai-solid/src/chat-ui/create-chat-hook.tsx | 1 - .../ai-solid/src/chat-ui/tool-approval.tsx | 121 ----- packages/ai-solid/src/create-chat-hook.ts | 1 - packages/ai-solid/src/index.ts | 7 +- packages/ai-solid/src/types.ts | 17 +- packages/ai-solid/src/ui.ts | 17 - packages/ai-solid/src/use-chat.ts | 26 +- packages/ai-solid/src/use-web-mcp-tools.ts | 12 - .../tests/chat-ui/coexistence.test.ts | 23 - packages/ai-solid/tests/test-utils.ts | 1 - packages/ai-solid/tests/use-chat.test.ts | 68 +-- .../ai-solid/tests/use-web-mcp-tools.test.ts | 7 - .../ai-svelte/src/chat-ui/create-chat-hook.ts | 1 - packages/ai-svelte/src/create-chat-hook.ts | 1 - packages/ai-svelte/src/create-chat.svelte.ts | 29 -- .../src/create-web-mcp-tools.svelte.ts | 12 - packages/ai-svelte/src/index.ts | 2 - packages/ai-svelte/src/types.ts | 22 +- .../tests/create-web-mcp-tools.test.ts | 9 +- packages/ai-svelte/tests/use-chat.test.ts | 17 +- packages/ai-vue-ui/CHANGELOG.md | 65 --- packages/ai-vue-ui/LICENSE | 21 - packages/ai-vue-ui/README.md | 29 -- packages/ai-vue-ui/package.json | 53 -- packages/ai-vue-ui/src/index.ts | 6 - packages/ai-vue-ui/src/vue-shim.d.ts | 9 - packages/ai-vue-ui/tsconfig.json | 11 - packages/ai-vue-ui/vite.config.ts | 14 - packages/ai-vue/src/chat-ui/chat-input.vue | 145 ------ packages/ai-vue/src/chat-ui/chat-message.vue | 81 ---- packages/ai-vue/src/chat-ui/chat-messages.vue | 68 --- packages/ai-vue/src/chat-ui/chat.vue | 43 -- .../ai-vue/src/chat-ui/create-chat-hook.ts | 1 - packages/ai-vue/src/chat-ui/message-part.vue | 128 ----- packages/ai-vue/src/chat-ui/tool-approval.vue | 75 --- packages/ai-vue/src/chat-ui/types.ts | 111 ----- .../ai-vue/src/chat-ui/use-chat-context.ts | 20 - packages/ai-vue/src/create-chat-hook.ts | 1 - packages/ai-vue/src/index.ts | 7 +- packages/ai-vue/src/types.ts | 17 +- packages/ai-vue/src/ui.ts | 18 +- packages/ai-vue/src/use-chat.ts | 38 +- packages/ai-vue/src/use-web-mcp-tools.ts | 12 - .../ai-vue/tests/chat-ui/coexistence.test.ts | 26 - packages/ai-vue/tests/test-utils.ts | 2 - packages/ai-vue/tests/use-chat.test.ts | 55 +-- .../ai-vue/tests/use-web-mcp-tools.test.ts | 12 +- packages/ai/src/activities/chat/index.ts | 45 +- .../src/activities/chat/middleware/types.ts | 10 +- .../src/activities/chat/stream/processor.ts | 229 +-------- .../ai/src/activities/chat/stream/types.ts | 1 - .../src/activities/generateVideo/adapter.ts | 26 +- .../ai/src/activities/generateVideo/index.ts | 5 +- packages/ai/src/interrupts.ts | 2 - packages/ai/src/middlewares/otel.ts | 8 - .../ai/src/middlewares/usage-attributes.ts | 9 +- packages/ai/src/scope.ts | 5 +- packages/ai/src/types.ts | 88 +--- .../ai/src/utilities/adapter-yield-chunk.ts | 3 - packages/ai/src/utilities/ag-ui-usage.test.ts | 4 - packages/ai/src/utilities/ag-ui-usage.ts | 7 - packages/ai/src/utilities/chat-params.ts | 6 - packages/ai/src/utilities/errors.ts | 12 +- .../utilities/normalize-stream-chunk.test.ts | 35 -- .../src/utilities/normalize-stream-chunk.ts | 20 - packages/ai/tests/chat-block-order.test.ts | 12 +- packages/ai/tests/chat-params.test.ts | 2 +- ...t-provider-executed-thinking-order.test.ts | 12 +- packages/ai/tests/chat.test.ts | 14 +- .../tests/custom-events-integration.test.ts | 74 --- packages/ai/tests/generate-voice.test.ts | 1 - packages/ai/tests/middleware.test.ts | 99 +--- .../middlewares/generation-middleware.test.ts | 60 +-- packages/ai/tests/middlewares/otel.test.ts | 13 +- packages/ai/tests/prompt-cache.test.ts | 6 - packages/ai/tests/rerank.test.ts | 3 - packages/ai/tests/stream-generation.test.ts | 6 +- packages/ai/tests/stream-processor.test.ts | 455 ++---------------- packages/ai/tests/type-check.test.ts | 39 +- packages/ai/tests/ui-message-metadata.test.ts | 8 +- .../src/adapters/chat-completions-text.ts | 58 --- .../src/adapters/responses-text.ts | 63 +-- .../openai-base/src/tools/apply-patch-tool.ts | 3 - .../src/tools/code-interpreter-tool.ts | 3 - .../src/tools/computer-use-tool.ts | 3 - packages/openai-base/src/tools/custom-tool.ts | 3 - .../openai-base/src/tools/file-search-tool.ts | 3 - .../openai-base/src/tools/function-tool.ts | 3 - .../src/tools/image-generation-tool.ts | 3 - .../openai-base/src/tools/local-shell-tool.ts | 3 - packages/openai-base/src/tools/mcp-tool.ts | 3 - packages/openai-base/src/tools/shell-tool.ts | 3 - .../src/tools/web-search-preview-tool.ts | 3 - .../openai-base/src/tools/web-search-tool.ts | 3 - ...chat-completions-incomplete-stream.test.ts | 2 - .../openai-base/tests/responses-text.test.ts | 20 - pnpm-lock.yaml | 43 +- testing/e2e/global-setup.ts | 24 +- testing/e2e/src/components/ApprovalPrompt.tsx | 38 -- testing/e2e/src/components/ChatUI.tsx | 23 - testing/e2e/src/lib/media-providers.ts | 2 +- testing/e2e/src/routes/$provider/$feature.tsx | 2 +- .../src/routes/api.gemini-image-ga-models.ts | 31 +- testing/e2e/src/routes/devtools-chat.tsx | 4 +- testing/e2e/src/routes/devtools-memory.tsx | 2 +- testing/e2e/src/routes/devtools-route-a.tsx | 4 +- testing/e2e/src/routes/devtools-route-b.tsx | 2 +- .../e2e/src/routes/devtools-structured.tsx | 2 +- testing/e2e/src/routes/devtools-subagents.tsx | 2 +- testing/e2e/src/routes/devtools-tools.tsx | 2 +- testing/e2e/src/routes/json-transport.tsx | 2 +- testing/e2e/src/routes/middleware-test.tsx | 9 +- .../e2e/src/routes/solid-reactive-chat.tsx | 2 +- testing/e2e/src/routes/subagents-test.tsx | 2 +- testing/e2e/src/routes/subagents-ui-test.tsx | 2 +- .../e2e/tests/gemini-image-ga-models.spec.ts | 17 +- testing/e2e/tests/middleware.spec.ts | 2 - testing/panel/src/routes/api.image.ts | 2 +- testing/panel/src/routes/api.video.ts | 2 +- testing/panel/src/routes/compaction.tsx | 2 +- testing/panel/src/routes/index.tsx | 54 +-- testing/panel/src/routes/memory.tsx | 2 +- testing/panel/src/routes/simulator.tsx | 54 +-- testing/panel/src/routes/skills.tsx | 2 +- testing/panel/src/routes/stream-debugger.tsx | 4 +- .../panel/src/routes/video-understanding.tsx | 2 +- .../generators/react-app/files/package.json | 1 - 387 files changed, 933 insertions(+), 9844 deletions(-) delete mode 100644 packages/ai-client/tests/chat-client-stop-continuation.test.ts delete mode 100644 packages/ai-client/tests/sse-done-model.test.ts delete mode 100644 packages/ai-react-ui/CHANGELOG.md delete mode 100644 packages/ai-react-ui/LICENSE delete mode 100644 packages/ai-react-ui/README.md delete mode 100644 packages/ai-react-ui/package.json delete mode 100644 packages/ai-react-ui/src/index.ts delete mode 100644 packages/ai-react-ui/tsconfig.json delete mode 100644 packages/ai-react-ui/vite.config.ts delete mode 100644 packages/ai-react/src/chat-ui/chat-input.tsx delete mode 100644 packages/ai-react/src/chat-ui/chat-message.tsx delete mode 100644 packages/ai-react/src/chat-ui/chat-messages.tsx delete mode 100644 packages/ai-react/src/chat-ui/chat.tsx delete mode 100644 packages/ai-react/src/chat-ui/tool-approval.tsx delete mode 100644 packages/ai-react/tests/chat-ui/coexistence.test.ts delete mode 100644 packages/ai-remix/src/chat-ui/chat-input.tsx delete mode 100644 packages/ai-remix/src/chat-ui/chat-message.tsx delete mode 100644 packages/ai-remix/src/chat-ui/chat-messages.tsx delete mode 100644 packages/ai-remix/src/chat-ui/chat.tsx delete mode 100644 packages/ai-remix/src/chat-ui/tool-approval.tsx delete mode 100644 packages/ai-solid-ui/CHANGELOG.md delete mode 100644 packages/ai-solid-ui/LICENSE delete mode 100644 packages/ai-solid-ui/README.md delete mode 100644 packages/ai-solid-ui/package.json delete mode 100644 packages/ai-solid-ui/src/index.ts delete mode 100644 packages/ai-solid-ui/tsconfig.json delete mode 100644 packages/ai-solid-ui/vite.config.ts delete mode 100644 packages/ai-solid/src/chat-ui/chat-input.tsx delete mode 100644 packages/ai-solid/src/chat-ui/chat-message.tsx delete mode 100644 packages/ai-solid/src/chat-ui/chat-messages.tsx delete mode 100644 packages/ai-solid/src/chat-ui/chat.tsx delete mode 100644 packages/ai-solid/src/chat-ui/tool-approval.tsx delete mode 100644 packages/ai-solid/tests/chat-ui/coexistence.test.ts delete mode 100644 packages/ai-vue-ui/CHANGELOG.md delete mode 100644 packages/ai-vue-ui/LICENSE delete mode 100644 packages/ai-vue-ui/README.md delete mode 100644 packages/ai-vue-ui/package.json delete mode 100644 packages/ai-vue-ui/src/index.ts delete mode 100644 packages/ai-vue-ui/src/vue-shim.d.ts delete mode 100644 packages/ai-vue-ui/tsconfig.json delete mode 100644 packages/ai-vue-ui/vite.config.ts delete mode 100644 packages/ai-vue/src/chat-ui/chat-input.vue delete mode 100644 packages/ai-vue/src/chat-ui/chat-message.vue delete mode 100644 packages/ai-vue/src/chat-ui/chat-messages.vue delete mode 100644 packages/ai-vue/src/chat-ui/chat.vue delete mode 100644 packages/ai-vue/src/chat-ui/message-part.vue delete mode 100644 packages/ai-vue/src/chat-ui/tool-approval.vue delete mode 100644 packages/ai-vue/src/chat-ui/use-chat-context.ts delete mode 100644 packages/ai-vue/tests/chat-ui/coexistence.test.ts delete mode 100644 testing/e2e/src/components/ApprovalPrompt.tsx diff --git a/examples/ag-ui/servers/bash/server.sh b/examples/ag-ui/servers/bash/server.sh index 1bd1fa9e0f..b85f0d91e2 100644 --- a/examples/ag-ui/servers/bash/server.sh +++ b/examples/ag-ui/servers/bash/server.sh @@ -15,10 +15,6 @@ write_event() { printf 'data: %s\n\n' "$1" } -write_done() { - printf 'data: [DONE]\n\n' -} - write_error_response() { local status="$1" local message="$2" @@ -218,7 +214,6 @@ handle_chat() { error: { message: $message } }' )" - write_done return fi @@ -310,7 +305,6 @@ handle_chat() { error: { message: $message } }' )" - write_done return fi @@ -330,7 +324,6 @@ handle_chat() { finishReason: "stop" }' )" - write_done } handle_connection() { diff --git a/examples/ag-ui/servers/go/agui.go b/examples/ag-ui/servers/go/agui.go index e8f10912a5..44bb548378 100644 --- a/examples/ag-ui/servers/go/agui.go +++ b/examples/ag-ui/servers/go/agui.go @@ -120,11 +120,6 @@ func (s *sseStream) writeEvent(payload map[string]any) error { return nil } -func (s *sseStream) writeDone() { - _, _ = fmt.Fprint(s.w, "data: [DONE]\n\n") - s.flusher.Flush() -} - func (s *sseStream) startRun() error { if s.started { return nil @@ -203,9 +198,5 @@ func (s *sseStream) streamText(stream func(emit func(string) error) error) error if err := stream(s.writeDelta); err != nil { return err } - if err := s.finishSuccess(); err != nil { - return err - } - s.writeDone() - return nil + return s.finishSuccess() } diff --git a/examples/ag-ui/servers/go/main.go b/examples/ag-ui/servers/go/main.go index 228d3769cf..1812380353 100644 --- a/examples/ag-ui/servers/go/main.go +++ b/examples/ag-ui/servers/go/main.go @@ -64,7 +64,6 @@ func handleChat(w http.ResponseWriter, r *http.Request) { system, messages := toChatMessages(input.Messages) if len(messages) == 0 { _ = stream.finishError("no user or assistant messages to send") - stream.writeDone() return } @@ -74,6 +73,5 @@ func handleChat(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("[go] chat error: %v", err) _ = stream.finishError(err.Error()) - stream.writeDone() } } diff --git a/examples/ag-ui/servers/php/lib/Agui.php b/examples/ag-ui/servers/php/lib/Agui.php index b00705b7f0..1401b9db7f 100644 --- a/examples/ag-ui/servers/php/lib/Agui.php +++ b/examples/ag-ui/servers/php/lib/Agui.php @@ -44,11 +44,6 @@ function sse_event(array $payload): string return 'data: ' . json_encode($payload, JSON_UNESCAPED_UNICODE) . "\n\n"; } -function sse_done(): string -{ - return "data: [DONE]\n\n"; -} - function run_started(string $threadId, string $runId): string { return sse_event([ @@ -152,11 +147,9 @@ function stream_text( $stream($emit); write_sse(text_message_end($messageId)); write_sse(run_finished($threadId, $runId)); - write_sse(sse_done()); } catch (Throwable $error) { error_log('[php] chat error: ' . $error->getMessage()); write_sse(run_error($threadId, $runId, $error->getMessage())); - write_sse(sse_done()); } } @@ -165,5 +158,4 @@ function stream_error_only(string $threadId, string $runId, string $message): vo begin_sse(); write_sse(run_started($threadId, $runId)); write_sse(run_error($threadId, $runId, $message)); - write_sse(sse_done()); } diff --git a/examples/ag-ui/servers/python/server.py b/examples/ag-ui/servers/python/server.py index cba8936cdf..5f22e397db 100644 --- a/examples/ag-ui/servers/python/server.py +++ b/examples/ag-ui/servers/python/server.py @@ -297,7 +297,6 @@ def do_POST(self) -> None: self.write_run_error( thread_id, run_id, "no user or assistant messages to send" ) - self.write_done() return message_id = f"msg-{run_id}" @@ -335,11 +334,9 @@ def do_POST(self) -> None: "finishReason": "stop", } ) - self.write_done() except Exception as error: self.log_message("chat error: %s", error) self.write_run_error(thread_id, run_id, str(error)) - self.write_done() def begin_sse(self) -> None: self.send_response(200) @@ -366,10 +363,6 @@ def write_run_error( } ) - def write_done(self) -> None: - self.wfile.write(b"data: [DONE]\n\n") - self.wfile.flush() - def main() -> None: server = ThreadingHTTPServer((HOST, PORT), AguiHandler) diff --git a/examples/ag-ui/servers/rust/src/agui.rs b/examples/ag-ui/servers/rust/src/agui.rs index 3ecdf1660f..fa7a7b10d9 100644 --- a/examples/ag-ui/servers/rust/src/agui.rs +++ b/examples/ag-ui/servers/rust/src/agui.rs @@ -78,10 +78,6 @@ pub fn sse_event(payload: Value) -> String { format!("data: {}\n\n", payload) } -pub fn sse_done() -> String { - "data: [DONE]\n\n".to_string() -} - pub fn run_started(thread_id: &str, run_id: &str) -> String { sse_event(json!({ "type": "RUN_STARTED", diff --git a/examples/ag-ui/servers/rust/src/main.rs b/examples/ag-ui/servers/rust/src/main.rs index 8451ffb291..1e8eefff48 100644 --- a/examples/ag-ui/servers/rust/src/main.rs +++ b/examples/ag-ui/servers/rust/src/main.rs @@ -3,7 +3,7 @@ mod messages; mod providers; use agui::{ - provider_from_input, run_error, run_finished, run_started, sse_done, text_message_content, + provider_from_input, run_error, run_finished, run_started, text_message_content, text_message_end, text_message_start, RunAgentInput, }; use async_stream::stream; @@ -79,7 +79,6 @@ async fn handle_chat(State(client): State, request: Request) -> Response &input.run_id, "no user or assistant messages to send", ), - sse_done(), ]; return sse_response(stream::iter( frames @@ -118,18 +117,13 @@ async fn handle_chat(State(client): State, request: Request) -> Response let _ = match result { Ok(()) => tx.send(format!( - "{}{}{}", + "{}{}", text_message_end(&llm_message_id), run_finished(&thread_id, &run_id), - sse_done() )), Err(message) => { eprintln!("[rust] chat error: {message}"); - tx.send(format!( - "{}{}", - run_error(&thread_id, &run_id, &message), - sse_done() - )) + tx.send(run_error(&thread_id, &run_id, &message)) } }; }); diff --git a/examples/ag-ui/servers/zig/src/agui.zig b/examples/ag-ui/servers/zig/src/agui.zig index 5f954326e1..8c9c23bf69 100644 --- a/examples/ag-ui/servers/zig/src/agui.zig +++ b/examples/ag-ui/servers/zig/src/agui.zig @@ -70,10 +70,6 @@ fn sseEvent(allocator: std.mem.Allocator, payload: anytype) ![]u8 { return std.fmt.allocPrint(allocator, "data: {s}\n\n", .{json}); } -pub fn sseDone(allocator: std.mem.Allocator) ![]u8 { - return try allocator.dupe(u8, "data: [DONE]\n\n"); -} - pub fn runStarted(allocator: std.mem.Allocator, thread_id: []const u8, run_id: []const u8) ![]u8 { return sseEvent(allocator, .{ .type = "RUN_STARTED", diff --git a/examples/ag-ui/servers/zig/src/main.zig b/examples/ag-ui/servers/zig/src/main.zig index be736807b4..2455803a9f 100644 --- a/examples/ag-ui/servers/zig/src/main.zig +++ b/examples/ag-ui/servers/zig/src/main.zig @@ -203,9 +203,6 @@ fn handleChat(allocator: std.mem.Allocator, io: Io, request: *std.http.Server.Re const frame = try agui.runError(allocator, input.threadId, input.runId, message); defer allocator.free(frame); try agui.writeSse(&response, frame); - const done = try agui.sseDone(allocator); - defer allocator.free(done); - try agui.writeSse(&response, done); try response.end(); return; }; @@ -218,9 +215,6 @@ fn handleChat(allocator: std.mem.Allocator, io: Io, request: *std.http.Server.Re defer allocator.free(finished); try agui.writeSse(&response, finished); - const done = try agui.sseDone(allocator); - defer allocator.free(done); - try agui.writeSse(&response, done); try response.end(); } @@ -251,8 +245,5 @@ fn streamErrorOnly( defer allocator.free(frame); try agui.writeSse(&response, frame); - const done = try agui.sseDone(allocator); - defer allocator.free(done); - try agui.writeSse(&response, done); try response.end(); } diff --git a/examples/ag-ui/src/App.tsx b/examples/ag-ui/src/App.tsx index 7e71453115..1121a9b7af 100644 --- a/examples/ag-ui/src/App.tsx +++ b/examples/ag-ui/src/App.tsx @@ -2,7 +2,7 @@ import { createContext, useContext, useEffect, useMemo, useState } from 'react' import { fetchServerSentEvents, useChat } from '@tanstack/ai-react' import type { ConnectionAdapter } from '@tanstack/ai-react' import { - ChatMessage, + ThinkingPart, createChatHookContexts, createChatUI, } from '@tanstack/ai-react/ui' @@ -78,9 +78,17 @@ const UI = createChatUI( ) }, - message: ({ message }) => , + message: ({ message, Parts }) => ( +
+ +
+ ), + }, + partsComponents: { + text: ({ part }) =>
{part.content}
, + thinking: ({ part }) => , + fallback: () => null, }, - partsComponents: { fallback: () => null }, }, ) diff --git a/examples/sandbox-cloudflare/src/routes/index.tsx b/examples/sandbox-cloudflare/src/routes/index.tsx index afb07f2812..be6b61b18e 100644 --- a/examples/sandbox-cloudflare/src/routes/index.tsx +++ b/examples/sandbox-cloudflare/src/routes/index.tsx @@ -385,7 +385,7 @@ function SandboxAgentPage() { const { messages, sendMessage, isLoading, stop, clear } = useChat({ connection: fetchServerSentEvents('/api/run'), - body, + forwardedProps: body, onChunk: (chunk) => { const sessionId = readHarnessSessionId(chunk, harness) if (sessionId !== undefined) setHarnessSessionId(sessionId) diff --git a/examples/sandbox-web/src/routes/index.tsx b/examples/sandbox-web/src/routes/index.tsx index d146caf42d..69b9517a01 100644 --- a/examples/sandbox-web/src/routes/index.tsx +++ b/examples/sandbox-web/src/routes/index.tsx @@ -355,7 +355,7 @@ function SandboxAgentChat({ initialThreadId }: { initialThreadId: string }) { return response }, }), - body, + forwardedProps: body, onChunk: (chunk) => { const sessionId = readSessionId(chunk) if (sessionId !== undefined) setHarnessSessionId(sessionId) diff --git a/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts b/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts index 335115b4be..3a56679ce5 100644 --- a/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts +++ b/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts @@ -210,7 +210,7 @@ export const Route = createFileRoute('/_banking-demo/api/banking-demo')({ const abortController = new AbortController() const body = await request.json() - const { messages, data } = body + const { messages, forwardedProps: data } = body const provider: Provider = data?.provider || 'anthropic' const model: string | undefined = data?.model diff --git a/examples/ts-code-mode-web/src/routes/_banking-demo/banking-demo.tsx b/examples/ts-code-mode-web/src/routes/_banking-demo/banking-demo.tsx index 94bb0601f5..3f4f66c69a 100644 --- a/examples/ts-code-mode-web/src/routes/_banking-demo/banking-demo.tsx +++ b/examples/ts-code-mode-web/src/routes/_banking-demo/banking-demo.tsx @@ -333,7 +333,7 @@ function BankingDemoPage() { const { messages, sendMessage, isLoading } = useChat({ connection: fetchServerSentEvents('/api/banking-demo'), - body, + forwardedProps: body, onCustomEvent: handleCustomEvent, }) diff --git a/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts b/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts index 4258f9ceb3..b32a851c95 100644 --- a/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts +++ b/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts @@ -237,7 +237,7 @@ export const Route = createFileRoute('/_database-demo/api/database-demo')({ const abortController = new AbortController() const body = await request.json() - const { messages, data } = body + const { messages, forwardedProps: data } = body const provider: Provider = data?.provider || 'anthropic' const model: string | undefined = data?.model diff --git a/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx b/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx index e55e250188..d3da461ce7 100644 --- a/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx +++ b/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx @@ -924,7 +924,7 @@ function DatabaseDemoPage() { const { messages, sendMessage, setMessages, isLoading } = useChat({ connection: fetchServerSentEvents('/api/database-demo'), - body, + forwardedProps: body, onCustomEvent: handleCustomEvent, }) diff --git a/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts b/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts index 2590e244a5..277ece25ec 100644 --- a/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts +++ b/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts @@ -232,7 +232,7 @@ export const Route = createFileRoute('/_home/api/product-codemode')({ const abortController = new AbortController() const body = await request.json() - const { messages, data } = body + const { messages, forwardedProps: data } = body const provider: Provider = data?.provider || 'anthropic' const model: string | undefined = data?.model diff --git a/examples/ts-code-mode-web/src/routes/_home/api.product-regular.ts b/examples/ts-code-mode-web/src/routes/_home/api.product-regular.ts index 00620e3b01..38d35fb12f 100644 --- a/examples/ts-code-mode-web/src/routes/_home/api.product-regular.ts +++ b/examples/ts-code-mode-web/src/routes/_home/api.product-regular.ts @@ -31,7 +31,7 @@ export const Route = createFileRoute('/_home/api/product-regular')({ const abortController = new AbortController() const body = await request.json() - const { messages, data } = body + const { messages, forwardedProps: data } = body const provider: Provider = data?.provider || 'anthropic' const model: string | undefined = data?.model diff --git a/examples/ts-code-mode-web/src/routes/_home/index.tsx b/examples/ts-code-mode-web/src/routes/_home/index.tsx index 64c0fb66cb..cf6e1098c0 100644 --- a/examples/ts-code-mode-web/src/routes/_home/index.tsx +++ b/examples/ts-code-mode-web/src/routes/_home/index.tsx @@ -704,7 +704,7 @@ function CodeModePanel({ const { messages, sendMessage, isLoading, stop } = useChat({ threadId: 'product-codemode', connection: fetchServerSentEvents('/api/product-codemode'), - body, + forwardedProps: body, onCustomEvent: handleCustomEvent, }) @@ -1002,7 +1002,7 @@ function RegularToolsPanel({ const { messages, sendMessage, isLoading, stop } = useChat({ threadId: 'product-regular', connection: fetchServerSentEvents('/api/product-regular'), - body, + forwardedProps: body, onCustomEvent: handleCustomEvent, }) diff --git a/examples/ts-code-mode-web/src/routes/_npm-github-chat/api.codemode.ts b/examples/ts-code-mode-web/src/routes/_npm-github-chat/api.codemode.ts index fe5fd74746..faf67a6bdb 100644 --- a/examples/ts-code-mode-web/src/routes/_npm-github-chat/api.codemode.ts +++ b/examples/ts-code-mode-web/src/routes/_npm-github-chat/api.codemode.ts @@ -62,7 +62,7 @@ export const Route = createFileRoute('/_npm-github-chat/api/codemode')({ const abortController = new AbortController() const body = await request.json() - const { messages, data } = body + const { messages, forwardedProps: data } = body const provider: Provider = data?.provider || 'anthropic' const model: string | undefined = data?.model diff --git a/examples/ts-code-mode-web/src/routes/_npm-github-chat/npm-github-chat.tsx b/examples/ts-code-mode-web/src/routes/_npm-github-chat/npm-github-chat.tsx index 31fb8c8b34..504e94d251 100644 --- a/examples/ts-code-mode-web/src/routes/_npm-github-chat/npm-github-chat.tsx +++ b/examples/ts-code-mode-web/src/routes/_npm-github-chat/npm-github-chat.tsx @@ -680,7 +680,7 @@ function CodeModePage() { const { messages, sendMessage, isLoading, stop } = useChat({ connection: fetchServerSentEvents('/api/codemode'), - body, + forwardedProps: body, onCustomEvent: handleCustomEvent, tools: [exportPdfClientTool], }) diff --git a/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts b/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts index 4a64e80fa1..51af1ecb09 100644 --- a/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts +++ b/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts @@ -60,7 +60,7 @@ export const Route = createFileRoute('/_reporting/api/reports')({ const abortController = new AbortController() const body = await request.json() - const { messages, data } = body + const { messages, forwardedProps: data } = body const provider: Provider = data?.provider || 'anthropic' const model: string | undefined = data?.model diff --git a/examples/ts-code-mode-web/src/routes/_reporting/reporting-agent.tsx b/examples/ts-code-mode-web/src/routes/_reporting/reporting-agent.tsx index a9467bccec..82800ebc54 100644 --- a/examples/ts-code-mode-web/src/routes/_reporting/reporting-agent.tsx +++ b/examples/ts-code-mode-web/src/routes/_reporting/reporting-agent.tsx @@ -577,7 +577,7 @@ function ReportingAgentPage() { const { messages, sendMessage, isLoading, stop } = useChat({ connection: fetchServerSentEvents('/api/reports'), - body, + forwardedProps: body, onCustomEvent: handleCustomEvent, }) diff --git a/examples/ts-react-chat/src/lib/sqlite-persistence.ts b/examples/ts-react-chat/src/lib/sqlite-persistence.ts index 12b179c7ad..668fae1e53 100644 --- a/examples/ts-react-chat/src/lib/sqlite-persistence.ts +++ b/examples/ts-react-chat/src/lib/sqlite-persistence.ts @@ -150,7 +150,7 @@ CREATE TABLE IF NOT EXISTS artifacts ( artifact_id text PRIMARY KEY NOT NULL, run_id text NOT NULL, thread_id text NOT NULL, - blob_key text, + blob_key text NOT NULL, name text NOT NULL, mime_type text NOT NULL, size integer NOT NULL, @@ -347,7 +347,7 @@ interface ArtifactRow { artifact_id: string run_id: string thread_id: string - blob_key: string | null + blob_key: string name: string mime_type: string size: number @@ -901,7 +901,7 @@ function mapArtifact(row: ArtifactRow): ArtifactRecord { artifactId: row.artifact_id, runId: row.run_id, threadId: row.thread_id, - ...(row.blob_key != null ? { blobKey: row.blob_key } : {}), + blobKey: row.blob_key, name: row.name, mimeType: row.mime_type, size: row.size, @@ -938,7 +938,7 @@ function createArtifactStore(db: DatabaseSync) { record.artifactId, record.runId, record.threadId, - record.blobKey ?? null, + record.blobKey, record.name, record.mimeType, record.size, diff --git a/examples/ts-react-chat/src/routes/capability-demo.tsx b/examples/ts-react-chat/src/routes/capability-demo.tsx index 7275a865d3..9c5f415a68 100644 --- a/examples/ts-react-chat/src/routes/capability-demo.tsx +++ b/examples/ts-react-chat/src/routes/capability-demo.tsx @@ -74,7 +74,7 @@ function ChatSurface({ config }: { config: SessionConfig }) { // Forwarded to the route as `forwardedProps`: the provider middleware // provides `capabilityValue`; `provideCapability=false` omits the provider // so chat() throws the capability validation error. - body: { + forwardedProps: { capabilityValue: config.persona, provideCapability: config.provideCapability, }, diff --git a/examples/ts-react-chat/src/routes/durable-persistence.tsx b/examples/ts-react-chat/src/routes/durable-persistence.tsx index 989df3a834..256d10789d 100644 --- a/examples/ts-react-chat/src/routes/durable-persistence.tsx +++ b/examples/ts-react-chat/src/routes/durable-persistence.tsx @@ -122,7 +122,7 @@ function ChatPane({ threadId }: { threadId: string }) { threadId, connection, persistence: true, - body: { oldBatching, slowModel }, + forwardedProps: { oldBatching, slowModel }, }) const [input, setInput] = useState( 'Write a 700-word story about a lighthouse keeper.', diff --git a/examples/ts-react-chat/src/routes/image-gen.tsx b/examples/ts-react-chat/src/routes/image-gen.tsx index 14b3733f42..c31b8730a3 100644 --- a/examples/ts-react-chat/src/routes/image-gen.tsx +++ b/examples/ts-react-chat/src/routes/image-gen.tsx @@ -22,10 +22,6 @@ interface ImageGenResult { const IMAGE_MODELS = [ { value: 'openai/gpt-5-image-mini', label: 'OpenAI GPT-5 Image Mini' }, { value: 'openai/gpt-5-image', label: 'OpenAI GPT-5 Image' }, - { - value: 'google/gemini-2.5-flash-image', - label: 'Gemini 2.5 Flash Image', - }, { value: 'google/gemini-nano-banana-2.1', label: 'Nano Banana 2.1', diff --git a/examples/ts-react-chat/src/routes/issue-176-tool-result.tsx b/examples/ts-react-chat/src/routes/issue-176-tool-result.tsx index 130275f945..e4e06c4307 100644 --- a/examples/ts-react-chat/src/routes/issue-176-tool-result.tsx +++ b/examples/ts-react-chat/src/routes/issue-176-tool-result.tsx @@ -60,7 +60,7 @@ function Issue176ToolResultRepro() { threadId: 'issue-176-live-tool-result-repro', connection: fetchServerSentEvents('/api/tanchat'), tools: liveTools, - body: { + forwardedProps: { provider: 'openai', model: 'gpt-5.5', }, diff --git a/examples/ts-react-chat/src/routes/mcp-apps.tsx b/examples/ts-react-chat/src/routes/mcp-apps.tsx index 23c5915c34..665be6f274 100644 --- a/examples/ts-react-chat/src/routes/mcp-apps.tsx +++ b/examples/ts-react-chat/src/routes/mcp-apps.tsx @@ -196,7 +196,7 @@ function McpAppsPage() { const { messages, sendMessage, isLoading, error, stop } = useChat({ threadId, connection: fetchServerSentEvents('/api/mcp-apps-chat'), - body: { provider }, + forwardedProps: { provider }, }) // One stable bridge per thread: routes the widget's tool calls to the diff --git a/examples/ts-react-chat/src/routes/mcp-demo.tsx b/examples/ts-react-chat/src/routes/mcp-demo.tsx index 71f0d05058..0ba93d6ebc 100644 --- a/examples/ts-react-chat/src/routes/mcp-demo.tsx +++ b/examples/ts-react-chat/src/routes/mcp-demo.tsx @@ -255,7 +255,7 @@ function ChatSurface({ connection: fetchServerSentEvents(endpoint), // `provider` is forwarded to the route (AG-UI forwardedProps); the route // resolves it to the matching text adapter. MCP works the same regardless. - body: { provider }, + forwardedProps: { provider }, }) const [input, setInput] = useState('') diff --git a/examples/ts-react-chat/src/routes/queueing.tsx b/examples/ts-react-chat/src/routes/queueing.tsx index a6ffbe8110..ae26ed0c5e 100644 --- a/examples/ts-react-chat/src/routes/queueing.tsx +++ b/examples/ts-react-chat/src/routes/queueing.tsx @@ -175,17 +175,17 @@ function QueueingPage() { // three via their sendMessage functions. const fifo = useChat({ connection: fetchServerSentEvents('/api/tanchat'), - body, + forwardedProps: body, queue: FIFO.config, }) const interrupt = useChat({ connection: fetchServerSentEvents('/api/tanchat'), - body, + forwardedProps: body, queue: INTERRUPT.config, }) const batch = useChat({ connection: fetchServerSentEvents('/api/tanchat'), - body, + forwardedProps: body, queue: BATCH.config, }) diff --git a/examples/ts-react-chat/src/routes/sandboxes-durable.tsx b/examples/ts-react-chat/src/routes/sandboxes-durable.tsx index 69bb1c2d18..79f99af5a4 100644 --- a/examples/ts-react-chat/src/routes/sandboxes-durable.tsx +++ b/examples/ts-react-chat/src/routes/sandboxes-durable.tsx @@ -687,7 +687,7 @@ function TriagePane({ // run through the replay branch. This flag plus a stable `threadId` is the // whole of what makes a refresh resume. persistence: true, - body: { + forwardedProps: { harness, provider, issueUrl, diff --git a/examples/ts-react-chat/src/routes/sandboxes.tsx b/examples/ts-react-chat/src/routes/sandboxes.tsx index 625828a4b6..5241b15812 100644 --- a/examples/ts-react-chat/src/routes/sandboxes.tsx +++ b/examples/ts-react-chat/src/routes/sandboxes.tsx @@ -384,7 +384,7 @@ function SandboxesPage() { const { messages, sendMessage, isLoading, stop, error } = useChat({ connection: fetchServerSentEvents('/api/sandbox-triage'), - body: { + forwardedProps: { harness, provider, issueUrl, diff --git a/examples/ts-react-chat/src/routes/threads.tsx b/examples/ts-react-chat/src/routes/threads.tsx index 766d83b71c..3d9fb49632 100644 --- a/examples/ts-react-chat/src/routes/threads.tsx +++ b/examples/ts-react-chat/src/routes/threads.tsx @@ -9,7 +9,11 @@ import rehypeSanitize from 'rehype-sanitize' import rehypeHighlight from 'rehype-highlight' import remarkGfm from 'remark-gfm' import { MessageSquare, Plus, Square, Trash2 } from 'lucide-react' -import type { ChatClientPersistence, UIMessage } from '@tanstack/ai-client' +import type { + ChatClientPersistence, + ChatPersistedState, + UIMessage, +} from '@tanstack/ai-client' import GuitarRecommendation from '@/components/example-GuitarRecommendation' import { addToCartToolDef, @@ -100,20 +104,21 @@ const threadPersistence: ChatClientPersistence = { if (!raw) return null // `UIMessage.createdAt` is a Date that JSON.stringify turned into a string — // revive it on read. - return (JSON.parse(raw) as Array).map((message) => ({ - ...message, - createdAt: - typeof message.createdAt === 'string' - ? new Date(message.createdAt) - : message.createdAt, - })) + const state = JSON.parse(raw) as ChatPersistedState + return { + ...state, + messages: state.messages.map((message) => ({ + ...message, + createdAt: + typeof message.createdAt === 'string' + ? new Date(message.createdAt) + : message.createdAt, + })), + } }, - setItem: (id, messages) => { + setItem: (id, state) => { if (!hasWindow()) return - window.localStorage.setItem( - THREAD_KEY_PREFIX + id, - JSON.stringify(messages), - ) + window.localStorage.setItem(THREAD_KEY_PREFIX + id, JSON.stringify(state)) }, removeItem: (id) => { if (!hasWindow()) return @@ -426,7 +431,7 @@ function ThreadChat({ connection: fetchServerSentEvents('/api/tanchat'), persistence: threadPersistence, tools, - body: CHAT_BODY, + forwardedProps: CHAT_BODY, onFinish: () => onActivity(), }) const [input, setInput] = useState('') diff --git a/examples/ts-react-chat/src/routes/typesafe-tools.tsx b/examples/ts-react-chat/src/routes/typesafe-tools.tsx index 39e49a818d..eb987fc71b 100644 --- a/examples/ts-react-chat/src/routes/typesafe-tools.tsx +++ b/examples/ts-react-chat/src/routes/typesafe-tools.tsx @@ -25,7 +25,7 @@ function TypesafeToolsPage() { const { messages, sendMessage, isLoading, error } = useChat({ threadId: 'typesafe-tools-bare-array', connection: fetchServerSentEvents('/api/tanchat'), - body: { provider: 'openai', model: 'gpt-5.5' }, + forwardedProps: { provider: 'openai', model: 'gpt-5.5' }, // 👇 Bare array literal — no clientTools(), no `as const`. tools: [ getGuitarsToolDef, diff --git a/examples/ts-solid-chat/src/routes/index.tsx b/examples/ts-solid-chat/src/routes/index.tsx index 7e215f42f0..628f7bf1ca 100644 --- a/examples/ts-solid-chat/src/routes/index.tsx +++ b/examples/ts-solid-chat/src/routes/index.tsx @@ -6,7 +6,11 @@ import { createChatClientOptions } from '@tanstack/ai-client' import { TextPart, ThinkingPart } from '@tanstack/ai-solid/ui' import { For, createSignal } from 'solid-js' -import type { InferChatMessages, UIMessage } from '@tanstack/ai-client' +import type { + BoundInterrupts, + InferChatMessages, + UIMessage, +} from '@tanstack/ai-client' import type { JSXElement } from 'solid-js' import GuitarRecommendation from '@/components/example-GuitarRecommendation' @@ -31,10 +35,7 @@ function ChatInputArea(props: { children: JSXElement }) { function Messages(props: { messages: Readonly - addToolApprovalResponse: (response: { - id: string - approved: boolean - }) => Promise + interrupts: BoundInterrupts }) { return (
@@ -93,57 +94,6 @@ function Messages(props: { ) } - // Approval UI. `'approval' in part` narrows the typed - // tool-call union to the tools declared `needsApproval: true`, - // which is where the `approval` field exists. - if ( - part.type === 'tool-call' && - part.state === 'approval-requested' && - 'approval' in part && - part.approval - ) { - return ( -
-

- 🔒 Approval Required: {part.name} -

-
-
-                                {JSON.stringify(
-                                  JSON.parse(part.arguments),
-                                  null,
-                                  2,
-                                )}
-                              
-
-
- - -
-
- ) - } - // Guitar recommendation card if ( part.type === 'tool-call' && @@ -170,6 +120,37 @@ function Messages(props: { ) } + {/* Tool approvals render from the bound `interrupts` list. */} + + {(interrupt) => + interrupt.kind === 'tool-approval' ? ( +
+

+ 🔒 Approval Required: {interrupt.toolName} +

+
+
+                  {JSON.stringify(interrupt.originalArgs, null, 2)}
+                
+
+
+ + +
+
+ ) : null + } +
) } @@ -317,13 +298,12 @@ function DebugPanel(props: { function ChatPage() { const [chunks, setChunks] = createSignal>([]) - const { messages, sendMessage, isLoading, addToolApprovalResponse, stop } = - useChat({ - ...chatOptions, - onChunk: (chunk: any) => { - setChunks((prev) => [...prev, chunk]) - }, - }) + const { messages, sendMessage, isLoading, interrupts, stop } = useChat({ + ...chatOptions, + onChunk: (chunk: any) => { + setChunks((prev) => [...prev, chunk]) + }, + }) const [input, setInput] = createSignal('') const clearChunks = () => setChunks([]) @@ -338,10 +318,7 @@ function ChatPage() { - +
diff --git a/examples/ts-svelte-chat/src/lib/components/Messages.svelte b/examples/ts-svelte-chat/src/lib/components/Messages.svelte index 1eca32c504..7677bf6200 100644 --- a/examples/ts-svelte-chat/src/lib/components/Messages.svelte +++ b/examples/ts-svelte-chat/src/lib/components/Messages.svelte @@ -9,13 +9,9 @@ interface Props { messages: ReadonlyArray - addToolApprovalResponse: (response: { - id: string - approved: boolean - }) => Promise } - let { messages, addToolApprovalResponse }: Props = $props() + let { messages }: Props = $props() let messagesContainer: HTMLDivElement | undefined = $state() @@ -89,44 +85,6 @@
{@html renderMarkdown(part.content)}
- {:else if part.type === 'tool-call' && part.state === 'approval-requested' && part.approval} -
-

- 🔒 Approval Required: {part.name} -

-
-
{JSON.stringify(
-                        JSON.parse(part.arguments),
-                        null,
-                        2,
-                      )}
-
-
- - -
-
{:else if part.type === 'tool-call' && part.name === 'recommendGuitar' && part.output}
diff --git a/examples/ts-svelte-chat/src/routes/+page.svelte b/examples/ts-svelte-chat/src/routes/+page.svelte index 24c5dcf8ce..70366aa6cb 100644 --- a/examples/ts-svelte-chat/src/routes/+page.svelte +++ b/examples/ts-svelte-chat/src/routes/+page.svelte @@ -66,7 +66,7 @@ const chat = createChat({ connection: fetchServerSentEvents('/api/chat'), tools, - get body() { + get forwardedProps() { return { provider: selectedModel.provider, model: selectedModel.model, @@ -121,10 +121,42 @@
- + + + + {#each chat.interrupts as interrupt (interrupt.id)} + {#if interrupt.kind === 'tool-approval'} +
+

+ 🔒 Approval Required: {interrupt.toolName} +

+
+
{JSON.stringify(
+                interrupt.originalArgs,
+                null,
+                2,
+              )}
+
+
+ + +
+
+ {/if} + {/each} () -const emit = defineEmits<{ - approve: [{ id: string; approved: boolean }] -}>() - const containerRef = ref() // Auto-scroll to bottom when messages change @@ -86,43 +82,6 @@ const isThinkingComplete = (message: UIMessage, partIndex: number) => { v-html="renderMarkdown(part.content)" /> - -
-

- 🔒 Approval Required: {{ part.name }} -

-
-
{{
-                  JSON.stringify(JSON.parse(part.arguments), null, 2)
-                }}
-
-
- - -
-
-
+ + + +
diff --git a/packages/ai-angular/src/index.ts b/packages/ai-angular/src/index.ts index e4957fa03c..b0130c5d21 100644 --- a/packages/ai-angular/src/index.ts +++ b/packages/ai-angular/src/index.ts @@ -6,12 +6,10 @@ export { injectByok } from './inject-byok' export { injectPageWebMCPTools, injectRegisterWebMCPTools, - injectWebMCPTools, } from './inject-web-mcp-tools' export type { InjectPageWebMCPToolsOptions, InjectRegisterWebMCPToolsOptions, - InjectWebMCPToolsOptions, } from './inject-web-mcp-tools' // Generation diff --git a/packages/ai-angular/src/inject-chat.ts b/packages/ai-angular/src/inject-chat.ts index 1514f1f22b..c3d49a0c5a 100644 --- a/packages/ai-angular/src/inject-chat.ts +++ b/packages/ai-angular/src/inject-chat.ts @@ -59,8 +59,6 @@ export function injectChat< const injector = inject(Injector) // Reactive option sources. Plain values become constant computeds. - const bodySource = - options.body !== undefined ? toReactive(options.body) : undefined const forwardedPropsSource = options.forwardedProps !== undefined ? toReactive(options.forwardedProps) @@ -103,7 +101,6 @@ export function injectChat< ...(options.initialResumeSnapshot !== undefined && { initialResumeSnapshot: options.initialResumeSnapshot, }), - ...(bodySource !== undefined && { body: bodySource() }), ...(forwardedPropsSource !== undefined && { forwardedProps: forwardedPropsSource(), }), @@ -150,15 +147,13 @@ export function injectChat< // matching `detach`. client.attach() - // Sync reactive body / forwardedProps / context to the client. - if (bodySource || forwardedPropsSource || contextSource) { + // Sync reactive forwardedProps / context to the client. + if (forwardedPropsSource || contextSource) { effect( () => { - const newBody = bodySource?.() const newForwardedProps = forwardedPropsSource?.() const newContext = contextSource?.() client.updateOptions({ - ...(newBody !== undefined && { body: newBody }), ...(newForwardedProps !== undefined && { forwardedProps: newForwardedProps, }), @@ -275,14 +270,7 @@ export function injectChat< }) => { await client.addToolResult(result) } - const addToolApprovalResponse = async (response: { - id: string - approved: boolean - }) => { - await client.addToolApprovalResponse(response) - } const interrupts = computed(() => interruptState().interrupts) - const pendingInterrupts = computed(() => interruptState().interrupts) const interruptErrors = computed(() => interruptState().interruptErrors) const resuming = computed(() => interruptState().resuming) const resolveInterrupts = ( @@ -326,10 +314,8 @@ export function injectChat< setMessages, clear, addToolResult, - addToolApprovalResponse, runId: computed(() => snapshot().runId), interrupts, - pendingInterrupts, interruptErrors, resuming, resolveInterrupts, diff --git a/packages/ai-angular/src/inject-web-mcp-tools.ts b/packages/ai-angular/src/inject-web-mcp-tools.ts index 81a37f0a8f..d7449de43e 100644 --- a/packages/ai-angular/src/inject-web-mcp-tools.ts +++ b/packages/ai-angular/src/inject-web-mcp-tools.ts @@ -26,12 +26,6 @@ export type InjectRegisterWebMCPToolsOptions< onError?: (error: unknown) => void } -/** @deprecated Use `InjectRegisterWebMCPToolsOptions`. Removed in 1.0.0. */ -export type InjectWebMCPToolsOptions< - TTools extends ReadonlyArray, - TContext = InferredClientContext, -> = InjectRegisterWebMCPToolsOptions - /** * Registers executable client tools with WebMCP for the current Angular owner. * @@ -74,12 +68,6 @@ export function injectRegisterWebMCPTools< }) } -/** - * @deprecated Use `injectRegisterWebMCPTools`. Removed in 1.0.0. - * @alias - */ -export const injectWebMCPTools = injectRegisterWebMCPTools - /** Options for {@link injectPageWebMCPTools}. */ export type InjectPageWebMCPToolsOptions = Omit< SubscribeWebMCPToolsOptions, diff --git a/packages/ai-angular/src/types.ts b/packages/ai-angular/src/types.ts index fc044ae599..745104817c 100644 --- a/packages/ai-angular/src/types.ts +++ b/packages/ai-angular/src/types.ts @@ -61,7 +61,7 @@ export type DeepPartial = * * Mirrors the Vue `useChat` options, except: * - State-change callbacks are managed internally and exposed as signals. - * - `tools`, `body`, `forwardedProps`, and `live` accept a {@link ReactiveOption} so + * - `tools`, `forwardedProps`, and `live` accept a {@link ReactiveOption} so * they can be a static value, a `Signal`, or a getter and stay reactive. */ export type InjectChatOptions< @@ -85,7 +85,6 @@ export type InjectChatOptions< | 'onRunIdChange' | 'context' | 'devtools' - | 'body' | 'forwardedProps' | 'tools' > & { @@ -93,9 +92,7 @@ export type InjectChatOptions< devtools?: AIDevtoolsDisplayOptions /** Client-side tools with execution logic. Reactive. */ tools?: ReactiveOption - /** Additional request body params. Reactive. */ - body?: ReactiveOption> - /** Forwarded request props (preferred over `body`). Reactive. */ + /** Forwarded request props. Reactive. */ forwardedProps?: ReactiveOption> /** Whether to keep a live subscription open. Reactive. */ live?: ReactiveOption @@ -160,11 +157,6 @@ interface BaseInjectChatResult< state?: 'output-available' | 'output-error' errorText?: string }) => Promise - /** Respond to a tool approval request. */ - addToolApprovalResponse: (response: { - id: string - approved: boolean - }) => Promise /** * The id of the run this client has in flight — one it started or rejoined — * or `null` when there is none (including while a run sits paused on an @@ -179,8 +171,6 @@ interface BaseInjectChatResult< runId: Signal /** Immutable bound interrupts for the current interrupted run. */ interrupts: Signal> - /** @deprecated Use `interrupts`. */ - pendingInterrupts: Signal> /** Batch-level interrupt errors. */ interruptErrors: Signal< ChatInterruptState['interruptErrors'] diff --git a/packages/ai-angular/tests/create-ui.test.ts b/packages/ai-angular/tests/create-ui.test.ts index 090f1ddfcc..00413b4698 100644 --- a/packages/ai-angular/tests/create-ui.test.ts +++ b/packages/ai-angular/tests/create-ui.test.ts @@ -22,7 +22,6 @@ function host( ...chat, messages: signal(chat.messages), interrupts: signal(chat.interrupts), - pendingInterrupts: signal(chat.pendingInterrupts), queue: signal(chat.queue), isLoading: signal(chat.isLoading), error: signal(chat.error), diff --git a/packages/ai-angular/tests/inject-chat.test.ts b/packages/ai-angular/tests/inject-chat.test.ts index 606a8bf759..c0ed826a2f 100644 --- a/packages/ai-angular/tests/inject-chat.test.ts +++ b/packages/ai-angular/tests/inject-chat.test.ts @@ -29,7 +29,6 @@ describe('injectChat', () => { }) expect(Object.isFrozen(result.interrupts())).toBe(true) - expect(result.pendingInterrupts()).toBe(result.interrupts()) expect(result.interrupts()[0]).toMatchObject({ id: 'staged-interrupt', status: 'pending', @@ -134,7 +133,7 @@ describe('injectChat — streaming', () => { }) const { result, flush } = renderInjectChat({ connection: adapter, - body: { provider: 'openai' }, + forwardedProps: { provider: 'openai' }, }) await result.sendMessage('Hi', { @@ -200,25 +199,28 @@ describe('injectChat — reactive options', () => { expect(result.isSubscribed()).toBe(false) }) - it('pushes body-signal changes to the client', async () => { + it('pushes forwardedProps-signal changes to the client', async () => { const updateSpy = vi.spyOn(ChatClient.prototype, 'updateOptions') try { const adapter = createMockConnectionAdapter() - const body = signal>({ model: 'a' }) - const { flush } = renderInjectChat({ connection: adapter, body }) + const forwardedProps = signal>({ model: 'a' }) + const { flush } = renderInjectChat({ + connection: adapter, + forwardedProps, + }) // initial effect run picks up { model: 'a' } flush() await tick() updateSpy.mockClear() - body.set({ model: 'b' }) + forwardedProps.set({ model: 'b' }) flush() await tick() expect(updateSpy).toHaveBeenCalled() expect(updateSpy).toHaveBeenCalledWith( - expect.objectContaining({ body: { model: 'b' } }), + expect.objectContaining({ forwardedProps: { model: 'b' } }), ) } finally { updateSpy.mockRestore() @@ -231,7 +233,7 @@ describe('injectChat — resume', () => { // `onResumeStateChange` is NOT an `injectChat` option (it is omitted here // exactly as in React / Solid / Vue / Svelte / Preact). The same // information reaches the caller as reactive state: the pending interrupts - // through `interrupts` / `pendingInterrupts` (and the + // through `interrupts` (and the // `onInterruptStateChange` callback), and the run identity through the // `runId` signal while a run is in flight. const onInterruptStateChange = vi.fn() @@ -273,7 +275,6 @@ describe('injectChat — resume', () => { expect(result.interrupts()).toEqual( expect.arrayContaining([expect.objectContaining({ id: 'interrupt-1' })]), ) - expect(result.pendingInterrupts()).toBe(result.interrupts()) expect(onInterruptStateChange).toHaveBeenCalledWith( expect.objectContaining({ interrupts: expect.arrayContaining([ diff --git a/packages/ai-angular/tests/inject-web-mcp-tools.test.ts b/packages/ai-angular/tests/inject-web-mcp-tools.test.ts index 7e3e6acf39..cf7f4feb63 100644 --- a/packages/ai-angular/tests/inject-web-mcp-tools.test.ts +++ b/packages/ai-angular/tests/inject-web-mcp-tools.test.ts @@ -7,7 +7,6 @@ import { injectChat, injectPageWebMCPTools, injectRegisterWebMCPTools, - injectWebMCPTools, type InjectRegisterWebMCPToolsOptions, } from '../src' import { createMockConnectionAdapter } from './test-utils' @@ -176,12 +175,6 @@ describe('injectRegisterWebMCPTools', () => { }) }) -describe('injectWebMCPTools', () => { - it('is a deprecated alias of injectRegisterWebMCPTools', () => { - expect(injectWebMCPTools).toBe(injectRegisterWebMCPTools) - }) -}) - function installPageTools(names: Array) { const target = new EventTarget() const modelContext = { diff --git a/packages/ai-angular/ui/src/create-chat-hook.ts b/packages/ai-angular/ui/src/create-chat-hook.ts index 9456d4e044..1e43d0090f 100644 --- a/packages/ai-angular/ui/src/create-chat-hook.ts +++ b/packages/ai-angular/ui/src/create-chat-hook.ts @@ -22,7 +22,6 @@ type ChatInstanceOverrides = { threadId?: string live?: HeadlessOptions['live'] forwardedProps?: HeadlessOptions['forwardedProps'] - body?: HeadlessOptions['body'] initialMessages?: HeadlessOptions['initialMessages'] } diff --git a/packages/ai-anthropic/src/adapters/text.ts b/packages/ai-anthropic/src/adapters/text.ts index d02313231b..7144c3df61 100644 --- a/packages/ai-anthropic/src/adapters/text.ts +++ b/packages/ai-anthropic/src/adapters/text.ts @@ -952,8 +952,7 @@ export class AnthropicTextAdapter< // Lift skills attached to a `code_execution` tool into the top-level // `container.skills` request param (Anthropic's required shape). Preserve any - // `container.id` supplied via modelOptions for container reuse. This is the - // canonical path for skills; `modelOptions.container.skills` is deprecated. + // `container.id` supplied via modelOptions for container reuse. const toolSkills = options.tools ?.map((tool) => getAnthropicProviderToolKind(tool) === 'code_execution' @@ -1508,7 +1507,6 @@ export class AnthropicTextAdapter< model: options.model, } let accumulatedContent = '' - let accumulatedThinking = '' let accumulatedSignature = '' const toolCallsMap = new Map< number, @@ -1539,7 +1537,6 @@ export class AnthropicTextAdapter< const runId = options.runId ?? genId() const threadId = options.threadId ?? genId() const messageId = genId() - let stepId: string | null = null let reasoningMessageId: string | null = null let hasClosedReasoning = false let hasEmittedRunStarted = false @@ -1678,11 +1675,9 @@ export class AnthropicTextAdapter< hasEmittedTextMessageStart = false } } else if (event.content_block.type === 'thinking') { - accumulatedThinking = '' accumulatedSignature = '' hasClosedReasoning = false - // Emit REASONING and STEP_STARTED for thinking - stepId = genId() + // Emit REASONING events for thinking reasoningMessageId = genId() // Spec REASONING events @@ -1699,22 +1694,11 @@ export class AnthropicTextAdapter< model, timestamp: Date.now(), } - - // Legacy STEP events (kept during transition) - yield { - type: EventType.STEP_STARTED, - stepName: stepId, - stepId, - model, - timestamp: Date.now(), - stepType: 'thinking', - } } else if (event.content_block.type === 'redacted_thinking') { // Encrypted thinking: no text, and its data must go back to // Anthropic unchanged. It gets its own reasoning message, and the // encrypted value names that message. The id prefix marks the - // data as a redacted block, not a signature. The step reuses the - // id so the stream processor sees the prefix too. + // data as a redacted block, not a signature. const redactedId = `${REDACTED_THINKING_ID_PREFIX}${genId()}` yield { type: EventType.REASONING_START, @@ -1729,23 +1713,6 @@ export class AnthropicTextAdapter< model, timestamp: Date.now(), } - yield { - type: EventType.STEP_STARTED, - stepName: redactedId, - stepId: redactedId, - model, - timestamp: Date.now(), - stepType: 'thinking', - } - yield { - type: EventType.STEP_FINISHED, - stepName: redactedId, - stepId: redactedId, - model, - timestamp: Date.now(), - delta: '', - content: '', - } yield { type: EventType.REASONING_ENCRYPTED_VALUE, subtype: 'message' as const, @@ -1813,7 +1780,6 @@ export class AnthropicTextAdapter< reasoningMessageId ) { const delta = event.delta.thinking - accumulatedThinking += delta // Spec REASONING content event yield { @@ -1823,17 +1789,6 @@ export class AnthropicTextAdapter< model, timestamp: Date.now(), } - - // Legacy STEP event - yield { - type: EventType.STEP_FINISHED, - stepName: stepId || genId(), - stepId: stepId || genId(), - model, - timestamp: Date.now(), - delta, - content: accumulatedThinking, - } } else if ( (event.delta as { type: string }).type === 'signature_delta' ) { @@ -1889,18 +1844,9 @@ export class AnthropicTextAdapter< } else if (event.type === 'content_block_stop') { if (currentBlockType === 'thinking') { // Emit signature so it can be replayed in multi-turn context. - // It belongs to the reasoning message, not the step, so an AG-UI - // client finds that message by `entityId`. - if (accumulatedSignature && stepId && reasoningMessageId) { - yield { - type: EventType.STEP_FINISHED, - stepName: stepId, - stepId, - model, - timestamp: Date.now(), - delta: '', - content: accumulatedThinking, - } + // It belongs to the reasoning message, so an AG-UI client finds that + // message by `entityId`. + if (accumulatedSignature && reasoningMessageId) { yield { type: EventType.REASONING_ENCRYPTED_VALUE, subtype: 'message' as const, diff --git a/packages/ai-anthropic/src/index.ts b/packages/ai-anthropic/src/index.ts index 0c489cb14b..a3efc190fb 100644 --- a/packages/ai-anthropic/src/index.ts +++ b/packages/ai-anthropic/src/index.ts @@ -59,7 +59,7 @@ export type { export { convertToolsToProviderFormat } from './tools/tool-converter' // Export tool types -export type { AnthropicTool, CustomTool } from './tools/index' +export type { AnthropicTool, CustomToolConfig } from './tools/index' // Export provider usage types export type { AnthropicProviderUsageDetails } from './usage' diff --git a/packages/ai-anthropic/src/message-types.ts b/packages/ai-anthropic/src/message-types.ts index fec7615383..977b0b290f 100644 --- a/packages/ai-anthropic/src/message-types.ts +++ b/packages/ai-anthropic/src/message-types.ts @@ -12,23 +12,10 @@ import type { TextCitationParam, } from '@anthropic-ai/sdk/resources' -/** - * Supported image media types for Anthropic. - */ -export type AnthropicImageMediaType = - | 'image/jpeg' - | 'image/png' - | 'image/gif' - | 'image/webp' - /** * Metadata for Anthropic image content parts. */ export interface AnthropicImageMetadata { - /** - * @deprecated Ignored. The media type is taken from `source.mimeType`. - */ - mediaType?: AnthropicImageMediaType /** * Cache control settings for the image content. */ @@ -45,19 +32,10 @@ export interface AnthropicTextMetadata { */ citations?: Array } -/** - * Supported document media types for Anthropic. - */ -export type AnthropicDocumentMediaType = 'application/pdf' - /** * Metadata for Anthropic document content parts (e.g., PDFs). */ export interface AnthropicDocumentMetadata { - /** - * @deprecated Ignored. The media type is taken from `source.mimeType`. - */ - mediaType?: AnthropicDocumentMediaType /** * Cache control settings for the document. */ diff --git a/packages/ai-anthropic/src/text/text-provider-options.ts b/packages/ai-anthropic/src/text/text-provider-options.ts index 430331a47a..4086ee8dce 100644 --- a/packages/ai-anthropic/src/text/text-provider-options.ts +++ b/packages/ai-anthropic/src/text/text-provider-options.ts @@ -59,20 +59,11 @@ export interface AnthropicCacheControlOptions { export interface AnthropicContainerOptions { /** - * Container identifier for reuse across requests. - * Container parameters with skills to be loaded. + * Container identifier for reuse across requests. To load skills into the + * container, use `codeExecutionTool(config, { skills })`. */ container?: { id: string | null - /** - * List of skills to load into the container. - * - * @deprecated Configure skills on the `code_execution` tool instead: - * `codeExecutionTool(config, { skills })`. The adapter lifts those into - * `container.skills` and attaches the required beta headers. Setting - * skills here bypasses the beta-header wiring and may stop working. - */ - skills: Array | null } | null } @@ -196,6 +187,15 @@ export interface InternalTextProviderOptions */ system?: string | Array + /** + * Container for the request. The adapter sets `skills` from the + * `code_execution` tool. + */ + container?: { + id: string | null + skills?: Array | null + } | null + /** `BetaTool`: the placeholder and the deferred tools of mid-conversation tool mode. */ tools?: Array diff --git a/packages/ai-anthropic/src/tools/bash-tool.ts b/packages/ai-anthropic/src/tools/bash-tool.ts index 61ed9fb1c9..ab9568bfd5 100644 --- a/packages/ai-anthropic/src/tools/bash-tool.ts +++ b/packages/ai-anthropic/src/tools/bash-tool.ts @@ -10,9 +10,6 @@ import type { ProviderTool, Tool } from '@tanstack/ai' export type BashToolConfig = BetaToolBash20241022 | BetaToolBash20250124 -/** @deprecated Renamed to `BashToolConfig`. Will be removed in a future release. */ -export type BashTool = BashToolConfig - export type AnthropicBashTool = ProviderTool<'anthropic', 'bash'> export function convertBashToolToAdapterFormat(tool: Tool): BashToolConfig { diff --git a/packages/ai-anthropic/src/tools/code-execution-tool.ts b/packages/ai-anthropic/src/tools/code-execution-tool.ts index e726f45c02..df31dd29a1 100644 --- a/packages/ai-anthropic/src/tools/code-execution-tool.ts +++ b/packages/ai-anthropic/src/tools/code-execution-tool.ts @@ -13,9 +13,6 @@ export type CodeExecutionToolConfig = | BetaCodeExecutionTool20250522 | BetaCodeExecutionTool20250825 -/** @deprecated Renamed to `CodeExecutionToolConfig`. Will be removed in a future release. */ -export type CodeExecutionTool = CodeExecutionToolConfig - /** * A hosted/managed Anthropic Skill reference. Lifted by the text adapter into * the top-level `container.skills` request param (NOT serialized into the diff --git a/packages/ai-anthropic/src/tools/computer-use-tool.ts b/packages/ai-anthropic/src/tools/computer-use-tool.ts index d37f5ce07b..1ed0e5097e 100644 --- a/packages/ai-anthropic/src/tools/computer-use-tool.ts +++ b/packages/ai-anthropic/src/tools/computer-use-tool.ts @@ -12,9 +12,6 @@ export type ComputerUseToolConfig = | BetaToolComputerUse20241022 | BetaToolComputerUse20250124 -/** @deprecated Renamed to `ComputerUseToolConfig`. Will be removed in a future release. */ -export type ComputerUseTool = ComputerUseToolConfig - export type AnthropicComputerUseTool = ProviderTool<'anthropic', 'computer_use'> export function convertComputerUseToolToAdapterFormat( diff --git a/packages/ai-anthropic/src/tools/custom-tool.ts b/packages/ai-anthropic/src/tools/custom-tool.ts index 582b7cc03b..ac8f8634ba 100644 --- a/packages/ai-anthropic/src/tools/custom-tool.ts +++ b/packages/ai-anthropic/src/tools/custom-tool.ts @@ -23,9 +23,6 @@ export interface CustomToolConfig { cache_control?: CacheControl | null } -/** @deprecated Renamed to `CustomToolConfig`. Will be removed in a future release. */ -export type CustomTool = CustomToolConfig - export function convertCustomToolToAdapterFormat(tool: Tool): CustomToolConfig { const metadata = (tool.metadata as { cacheControl?: CacheControl | null } | undefined) || {} diff --git a/packages/ai-anthropic/src/tools/index.ts b/packages/ai-anthropic/src/tools/index.ts index 9ad77c0431..93be0e9b9c 100644 --- a/packages/ai-anthropic/src/tools/index.ts +++ b/packages/ai-anthropic/src/tools/index.ts @@ -1,17 +1,16 @@ -import type { BashTool } from './bash-tool' -import type { CodeExecutionTool } from './code-execution-tool' -import type { ComputerUseTool } from './computer-use-tool' -import type { CustomTool } from './custom-tool' -import type { MemoryTool } from './memory-tool' -import type { TextEditorTool } from './text-editor-tool' -import type { WebFetchTool } from './web-fetch-tool' -import type { WebSearchTool } from './web-search-tool' +import type { BashToolConfig } from './bash-tool' +import type { CodeExecutionToolConfig } from './code-execution-tool' +import type { ComputerUseToolConfig } from './computer-use-tool' +import type { CustomToolConfig } from './custom-tool' +import type { MemoryToolConfig } from './memory-tool' +import type { TextEditorToolConfig } from './text-editor-tool' +import type { WebFetchToolConfig } from './web-fetch-tool' +import type { WebSearchToolConfig } from './web-search-tool' export { bashTool, type AnthropicBashTool, type BashToolConfig, - type BashTool, } from './bash-tool' export { codeExecutionTool, @@ -22,51 +21,41 @@ export { type AnthropicContainerSkill, type CodeExecutionToolConfig, type CodeExecutionToolOptions, - type CodeExecutionTool, } from './code-execution-tool' export { computerUseTool, type AnthropicComputerUseTool, type ComputerUseToolConfig, - type ComputerUseTool, } from './computer-use-tool' -export { - customTool, - type CustomToolConfig, - type CustomTool, -} from './custom-tool' +export { customTool, type CustomToolConfig } from './custom-tool' export { memoryTool, type AnthropicMemoryTool, type MemoryToolConfig, - type MemoryTool, } from './memory-tool' export { textEditorTool, type AnthropicTextEditorTool, type TextEditorToolConfig, - type TextEditorTool, } from './text-editor-tool' export { webFetchTool, type AnthropicWebFetchTool, type WebFetchToolConfig, - type WebFetchTool, } from './web-fetch-tool' export { webSearchTool, type AnthropicWebSearchTool, type WebSearchToolConfig, - type WebSearchTool, } from './web-search-tool' // Keep the discriminated union defined inline (no barrel exports). export type AnthropicTool = - | BashTool - | CodeExecutionTool - | ComputerUseTool - | CustomTool - | MemoryTool - | TextEditorTool - | WebFetchTool - | WebSearchTool + | BashToolConfig + | CodeExecutionToolConfig + | ComputerUseToolConfig + | CustomToolConfig + | MemoryToolConfig + | TextEditorToolConfig + | WebFetchToolConfig + | WebSearchToolConfig diff --git a/packages/ai-anthropic/src/tools/memory-tool.ts b/packages/ai-anthropic/src/tools/memory-tool.ts index fd80e5c6fa..c099925178 100644 --- a/packages/ai-anthropic/src/tools/memory-tool.ts +++ b/packages/ai-anthropic/src/tools/memory-tool.ts @@ -7,9 +7,6 @@ import type { ProviderTool, Tool } from '@tanstack/ai' export type MemoryToolConfig = BetaMemoryTool20250818 -/** @deprecated Renamed to `MemoryToolConfig`. Will be removed in a future release. */ -export type MemoryTool = MemoryToolConfig - export type AnthropicMemoryTool = ProviderTool<'anthropic', 'memory'> export function convertMemoryToolToAdapterFormat(tool: Tool): MemoryToolConfig { diff --git a/packages/ai-anthropic/src/tools/text-editor-tool.ts b/packages/ai-anthropic/src/tools/text-editor-tool.ts index ccfbf53e18..9746fd2162 100644 --- a/packages/ai-anthropic/src/tools/text-editor-tool.ts +++ b/packages/ai-anthropic/src/tools/text-editor-tool.ts @@ -14,9 +14,6 @@ export type TextEditorToolConfig = | ToolTextEditor20250429 | ToolTextEditor20250728 -/** @deprecated Renamed to `TextEditorToolConfig`. Will be removed in a future release. */ -export type TextEditorTool = TextEditorToolConfig - export type AnthropicTextEditorTool = ProviderTool<'anthropic', 'text_editor'> export function convertTextEditorToolToAdapterFormat( diff --git a/packages/ai-anthropic/src/tools/web-fetch-tool.ts b/packages/ai-anthropic/src/tools/web-fetch-tool.ts index d770654910..96f63304dd 100644 --- a/packages/ai-anthropic/src/tools/web-fetch-tool.ts +++ b/packages/ai-anthropic/src/tools/web-fetch-tool.ts @@ -7,9 +7,6 @@ import type { ProviderTool, Tool } from '@tanstack/ai' export type WebFetchToolConfig = BetaWebFetchTool20250910 -/** @deprecated Renamed to `WebFetchToolConfig`. Will be removed in a future release. */ -export type WebFetchTool = WebFetchToolConfig - export type AnthropicWebFetchTool = ProviderTool<'anthropic', 'web_fetch'> export function convertWebFetchToolToAdapterFormat( diff --git a/packages/ai-anthropic/src/tools/web-search-tool.ts b/packages/ai-anthropic/src/tools/web-search-tool.ts index 30081c055d..506ddd8280 100644 --- a/packages/ai-anthropic/src/tools/web-search-tool.ts +++ b/packages/ai-anthropic/src/tools/web-search-tool.ts @@ -7,9 +7,6 @@ import type { ProviderTool, Tool } from '@tanstack/ai' export type WebSearchToolConfig = WebSearchTool20250305 -/** @deprecated Renamed to `WebSearchToolConfig`. Will be removed in a future release. */ -export type WebSearchTool = WebSearchToolConfig - export type AnthropicWebSearchTool = ProviderTool<'anthropic', 'web_search'> const validateDomains = (tool: WebSearchToolConfig) => { diff --git a/packages/ai-anthropic/tests/anthropic-adapter.test.ts b/packages/ai-anthropic/tests/anthropic-adapter.test.ts index 4c51a2db89..ef3529dd66 100644 --- a/packages/ai-anthropic/tests/anthropic-adapter.test.ts +++ b/packages/ai-anthropic/tests/anthropic-adapter.test.ts @@ -206,7 +206,6 @@ describe('Anthropic adapter option mapping', () => { const adapter = createAdapter('claude-opus-4-1') const metadata = { - mediaType: 'application/pdf', cache_control: { type: 'ephemeral' }, citations: { enabled: true }, context: 'Quarterly report', @@ -276,7 +275,6 @@ describe('Anthropic adapter option mapping', () => { type: 'image', source: { type: 'url', value: 'https://example.com/a.png' }, metadata: { - mediaType: 'image/png', cache_control: { type: 'ephemeral' }, filename: 'a.png', }, @@ -494,7 +492,6 @@ describe('Anthropic adapter option mapping', () => { const providerOptions = { container: { id: 'container-weather', - skills: [{ skill_id: 'forecast', type: 'custom', version: '1' }], }, mcp_servers: [ { diff --git a/packages/ai-anthropic/tests/text.skills.test.ts b/packages/ai-anthropic/tests/text.skills.test.ts index a7ac97048e..0b90d3101b 100644 --- a/packages/ai-anthropic/tests/text.skills.test.ts +++ b/packages/ai-anthropic/tests/text.skills.test.ts @@ -43,7 +43,7 @@ describe('anthropic skills → container', () => { const req = adapter.mapCommonOptionsToAnthropic( baseOptions({ tools: [tool], - modelOptions: { container: { id: 'ctr_1', skills: null } }, + modelOptions: { container: { id: 'ctr_1' } }, }), ) expect(req.container?.id).toBe('ctr_1') diff --git a/packages/ai-bedrock/src/converse/stream-processor.ts b/packages/ai-bedrock/src/converse/stream-processor.ts index 2be2caa5cb..c80c6c2d1e 100644 --- a/packages/ai-bedrock/src/converse/stream-processor.ts +++ b/packages/ai-bedrock/src/converse/stream-processor.ts @@ -54,7 +54,6 @@ export async function* processConverseStream( number, { id: string - text: string signature: string bytes: Array redacted: boolean @@ -111,11 +110,6 @@ export async function* processConverseStream( entityId: reasoning.id, subtype: 'message', encryptedValue: encrypted, - ...(reasoning.redacted && { - stepId: reasoning.id.startsWith('redacted_thinking-') - ? reasoning.id - : 'redacted_thinking-' + reasoning.id, - }), timestamp: Date.now(), } } @@ -126,14 +120,6 @@ export async function* processConverseStream( yield* encryptedReasoning(blockIndex) if (reasoning.closed) continue reasoning.closed = true - yield { - type: EventType.STEP_FINISHED, - stepId: reasoning.id, - stepName: reasoning.id, - delta: '', - content: reasoning.text, - timestamp: Date.now(), - } yield { type: EventType.REASONING_MESSAGE_END, messageId: reasoning.id, @@ -211,20 +197,12 @@ export async function* processConverseStream( (fragment.redactedContent?.length ?? 0) > 0 reasoning = { id: (redacted ? 'redacted_thinking-' : '') + newMessageId(), - text: '', signature: '', bytes: [], redacted, closed: false, } reasoningByIndex.set(index, reasoning) - yield { - type: EventType.STEP_STARTED, - stepId: reasoning.id, - stepName: reasoning.id, - stepType: 'thinking', - timestamp: Date.now(), - } yield { type: EventType.REASONING_MESSAGE_START, messageId: reasoning.id, @@ -237,7 +215,6 @@ export async function* processConverseStream( 'text' in fragment && fragment.text !== undefined ) { - reasoning.text += fragment.text yield { type: EventType.REASONING_MESSAGE_CONTENT, messageId: reasoning.id, @@ -258,7 +235,6 @@ export async function* processConverseStream( ) { if (!reasoning.redacted) reasoning.encrypted = undefined reasoning.redacted = true - reasoning.text = '' reasoning.signature = '' reasoning.bytes.push(fragment.redactedContent) yield* encryptedReasoning(index) diff --git a/packages/ai-bedrock/tests/replay-parity.test.ts b/packages/ai-bedrock/tests/replay-parity.test.ts index b18ae00d25..2b383b6ca0 100644 --- a/packages/ai-bedrock/tests/replay-parity.test.ts +++ b/packages/ai-bedrock/tests/replay-parity.test.ts @@ -253,7 +253,9 @@ describe('Bedrock replay parity', () => { expect(new Set(signatures.map((chunk) => chunk.entityId)).size).toBe(3) expect(signatures[2]?.entityId).toMatch(/^redacted_thinking-/) expect( - chunks.filter((chunk) => chunk.type === EventType.STEP_STARTED), + chunks.filter( + (chunk) => chunk.type === EventType.REASONING_MESSAGE_START, + ), ).toHaveLength(3) expect( chunks.find((chunk) => chunk.type === EventType.RUN_FINISHED), @@ -592,8 +594,5 @@ it('marks a late redacted transition even when the encoded signature is unchange (chunk) => chunk.type === EventType.REASONING_ENCRYPTED_VALUE, ) expect(encrypted).toHaveLength(2) - expect(encrypted[1]).toMatchObject({ - encryptedValue: 'AAH/', - stepId: expect.stringMatching(/^redacted_thinking-/), - }) + expect(encrypted[1]).toMatchObject({ encryptedValue: 'AAH/' }) }) diff --git a/packages/ai-byteplus/src/adapters/image.ts b/packages/ai-byteplus/src/adapters/image.ts index 10d7b9fa23..b68cddf7e0 100644 --- a/packages/ai-byteplus/src/adapters/image.ts +++ b/packages/ai-byteplus/src/adapters/image.ts @@ -100,8 +100,7 @@ function describeFailures( * BytePlus bills per generated image and does not count input tokens, so * `promptTokens` is always 0 and `generated_images` is surfaced as * `usage.billed` (`{ quantity, unit: 'images' }`) — the count the price is - * applied to. The deprecated `unitsBilled` is still populated for - * backward compatibility. + * applied to. */ function buildBytePlusImageUsage( usage: BytePlusImageUsage | undefined, @@ -115,7 +114,6 @@ function buildBytePlusImageUsage( totalTokens: usage.total_tokens ?? completionTokens, ...(usage.generated_images !== undefined && { billed: { quantity: usage.generated_images, unit: 'images' }, - unitsBilled: usage.generated_images, }), } } diff --git a/packages/ai-byteplus/src/adapters/text.ts b/packages/ai-byteplus/src/adapters/text.ts index c88557e296..5998add018 100644 --- a/packages/ai-byteplus/src/adapters/text.ts +++ b/packages/ai-byteplus/src/adapters/text.ts @@ -199,9 +199,9 @@ export class BytePlusTextAdapter< /** * Captures Ark's `encrypted_content` and sends it as a - * `REASONING_ENCRYPTED_VALUE` for the reasoning message, right after the - * reasoning step's `STEP_FINISHED`. Its `entityId` is the reasoning - * message id, so an AG-UI client finds that message. + * `REASONING_ENCRYPTED_VALUE` for the reasoning message, right before its + * `REASONING_MESSAGE_END`. Its `entityId` is the reasoning message id, so an + * AG-UI client finds that message. * * On a thinking-summary model Ark streams the whole blob as one dedicated * chunk (empty `content` and `reasoning_content`) sitting between the @@ -242,12 +242,10 @@ export class BytePlusTextAdapter< reasoningMessageId = event.messageId } if ( - event.type === EventType.STEP_FINISHED && + event.type === EventType.REASONING_MESSAGE_END && captured.encryptedContent !== undefined && - event.signature === undefined && - reasoningMessageId !== undefined + event.messageId === reasoningMessageId ) { - yield event yield { type: EventType.REASONING_ENCRYPTED_VALUE, subtype: 'message' as const, @@ -256,7 +254,6 @@ export class BytePlusTextAdapter< model: event.model, timestamp: event.timestamp, } - continue } yield event } diff --git a/packages/ai-byteplus/src/adapters/transcription.ts b/packages/ai-byteplus/src/adapters/transcription.ts index 4a57f042ca..feecdebca4 100644 --- a/packages/ai-byteplus/src/adapters/transcription.ts +++ b/packages/ai-byteplus/src/adapters/transcription.ts @@ -316,8 +316,7 @@ export function mapRecognizeResponse( // Seed ASR is duration-billed and reports no token counts, so `usage` // carries only the audio length — the same shape the Grok and OpenAI - // whisper paths use. `durationSeconds` is deprecated but still populated - // alongside the self-describing `billed` pair. + // whisper paths use. const usage: TokenUsage | undefined = duration !== undefined ? { @@ -325,7 +324,6 @@ export function mapRecognizeResponse( completionTokens: 0, totalTokens: 0, billed: { quantity: duration, unit: 'seconds' }, - durationSeconds: duration, } : undefined diff --git a/packages/ai-byteplus/src/adapters/video.ts b/packages/ai-byteplus/src/adapters/video.ts index d6ce65cda2..e1a9998c9f 100644 --- a/packages/ai-byteplus/src/adapters/video.ts +++ b/packages/ai-byteplus/src/adapters/video.ts @@ -103,8 +103,7 @@ function toTokenCount(value: number | string | undefined): number | undefined { * Seedance bills output only. The API documents input tokens as always 0 and * `total_tokens` as equal to `completion_tokens`, so `promptTokens` is 0 and * the completion count is the billed quantity (`usage.billed` with - * `unit: 'tokens'`). The deprecated `unitsBilled` is still populated for - * backward compatibility. + * `unit: 'tokens'`). */ function buildBytePlusVideoUsage( usage: BytePlusVideoTaskUsage | undefined, @@ -123,7 +122,6 @@ function buildBytePlusVideoUsage( completionTokens: completion, totalTokens: totalTokens ?? completion, billed: { quantity: completion, unit: 'tokens' }, - unitsBilled: completion, } } diff --git a/packages/ai-byteplus/tests/image.test.ts b/packages/ai-byteplus/tests/image.test.ts index 625eeb1b28..9cb77d552f 100644 --- a/packages/ai-byteplus/tests/image.test.ts +++ b/packages/ai-byteplus/tests/image.test.ts @@ -502,7 +502,6 @@ describe('response mapping', () => { completionTokens: 3888, totalTokens: 3888, billed: { quantity: 1, unit: 'images' }, - unitsBilled: 1, }) }) @@ -543,7 +542,6 @@ describe('response mapping', () => { { url: 'https://example.com/1.png' }, { b64Json: 'QUJD' }, ]) - expect(result.usage?.unitsBilled).toBe(2) expect(result.usage?.billed).toEqual({ quantity: 2, unit: 'images' }) }) diff --git a/packages/ai-byteplus/tests/text.test.ts b/packages/ai-byteplus/tests/text.test.ts index 0f3fde0e2d..1462c6fc6c 100644 --- a/packages/ai-byteplus/tests/text.test.ts +++ b/packages/ai-byteplus/tests/text.test.ts @@ -350,12 +350,12 @@ describe('BytePlus text adapter', () => { // Pinning the consequence, not endorsing it: with no reasoning deltas // the base never opens a reasoning lifecycle, so there is no - // STEP_FINISHED to stamp and the blob is silently dropped. Live Ark - // always sends the encrypted chunk *after* reasoning deltas, but that - // ordering is observed behaviour rather than a documented contract — if - // this assertion ever fails because a STEP_FINISHED appeared, the - // capture path needs revisiting, not this expectation. - expect(chunks.some((c) => c.type === EventType.STEP_FINISHED)).toBe(false) + // REASONING_MESSAGE_END to attach to and the blob is silently dropped. + // Live Ark always sends the encrypted chunk *after* reasoning deltas, but + // that ordering is observed behaviour rather than a documented contract. + expect( + chunks.some((c) => c.type === EventType.REASONING_ENCRYPTED_VALUE), + ).toBe(false) }) it('emits AG-UI tool-call events with the OpenAI tool shape', async () => { @@ -1118,16 +1118,9 @@ describe('encrypted_content through the chat() agent loop', () => { }) }) -// The adapter stamps `delta` onto a STEP_FINISHED that already carries the -// same reasoning text via REASONING_MESSAGE_CONTENT, because chat()'s agent -// loop reads thinking only from STEP_FINISHED.delta and drops the whole step — -// signature included — when that accumulation is empty. -// -// Nothing double-counts today only because StreamProcessor short-circuits -// STEP_FINISHED content once it has seen reasoning events. That guarantee -// lives in @tanstack/ai, not here, so without this test a change over there -// would double every BytePlus reasoning block in the UI while this package -// stayed green. +// The thinking text arrives only through REASONING_MESSAGE_CONTENT, and the +// signature through REASONING_ENCRYPTED_VALUE. This test guards that +// StreamProcessor builds one thinking part with both. describe('reasoning delta round-trip through StreamProcessor', () => { it('surfaces the thinking text once, with the signature attached', async () => { setupMockSdkClient(thinkingStreamChunks(THINKING_SUMMARY_MODEL)) @@ -1148,8 +1141,7 @@ describe('reasoning delta round-trip through StreamProcessor', () => { .filter((part) => part.type === 'thinking') expect(thinkingParts).toHaveLength(1) - // Not 'The user wants a greeting.The user wants a greeting.' — the exact - // failure the STEP_FINISHED short-circuit prevents. + // Not 'The user wants a greeting.The user wants a greeting.' expect(thinkingParts[0]).toMatchObject({ content: 'The user wants a greeting.', signature: ENCRYPTED_BLOB, diff --git a/packages/ai-byteplus/tests/transcription.test.ts b/packages/ai-byteplus/tests/transcription.test.ts index 6bf39bde7f..8da273bb7f 100644 --- a/packages/ai-byteplus/tests/transcription.test.ts +++ b/packages/ai-byteplus/tests/transcription.test.ts @@ -133,7 +133,6 @@ describe('BytePlusTranscriptionAdapter', () => { completionTokens: 0, totalTokens: 0, billed: { quantity: 2.499, unit: 'seconds' }, - durationSeconds: 2.499, }) expect(result.id).toMatch(/^byteplus-/) }) diff --git a/packages/ai-byteplus/tests/video.test.ts b/packages/ai-byteplus/tests/video.test.ts index 544db0927e..22e3efb7d0 100644 --- a/packages/ai-byteplus/tests/video.test.ts +++ b/packages/ai-byteplus/tests/video.test.ts @@ -965,7 +965,6 @@ describe('getVideo', () => { completionTokens: 35800, totalTokens: 35800, billed: { quantity: 35800, unit: 'tokens' }, - unitsBilled: 35800, }) }) @@ -1001,7 +1000,6 @@ describe('getVideo', () => { completionTokens: 35800, totalTokens: 35800, billed: { quantity: 35800, unit: 'tokens' }, - unitsBilled: 35800, }) }) diff --git a/packages/ai-client/src/chat-client.ts b/packages/ai-client/src/chat-client.ts index 44138c6a0f..47061a1322 100644 --- a/packages/ai-client/src/chat-client.ts +++ b/packages/ai-client/src/chat-client.ts @@ -134,8 +134,6 @@ type ChatClientUpdateOptionsWithoutContext< > = { connection?: ConnectionAdapter fetcher?: ChatFetcher - /** @deprecated Use `forwardedProps` instead. */ - body?: Record forwardedProps?: Record byok?: ByokClient byokProvider?: () => string | undefined @@ -375,9 +373,6 @@ function rebuildsAssistantMessage(chunk: StreamChunk): boolean { chunk.encryptedValue.length > 0 ) } - if (chunk.type === 'STEP_FINISHED') { - return 'signature' in chunk && Boolean(chunk.signature) - } return REJOIN_REBUILD_TRIGGERS.has(chunk.type) } @@ -481,7 +476,6 @@ export class ChatClient< isHydrating: false, interruptState: { interrupts: Object.freeze([]), - pendingInterrupts: Object.freeze([]), interruptErrors: Object.freeze([]), resuming: false, } as ChatInterruptState, @@ -538,11 +532,6 @@ export class ChatClient< private pendingResumeItems: Array | null = null private activeResumeThreadId: string | null = null private activeResumeRunId: string | null = null - // Track the legacy `body` option and the canonical `forwardedProps` - // option as separate slots so that `updateOptions({ forwardedProps })` - // doesn't wipe a previously-set `body` (and vice versa). They are - // merged on every send, with `forwardedProps` winning on key collision. - private bodyOption: Record = {} private forwardedPropsOption: Record = {} private byok: ByokClient | undefined private byokProvider: (() => string | undefined) | undefined @@ -711,12 +700,6 @@ export class ChatClient< } else { this.historyPageSize = undefined } - // Both `body` (deprecated) and `forwardedProps` populate the AG-UI - // `RunAgentInput.forwardedProps` wire field. They are stored - // separately so `updateOptions` can replace one without touching the - // other; the merge happens at send time, with `forwardedProps` - // winning on key collision. - this.bodyOption = options.body || {} this.forwardedPropsOption = options.forwardedProps || {} this.byok = options.byok this.byokProvider = options.byokProvider @@ -1669,14 +1652,6 @@ export class ChatClient< > } - /** @deprecated Use getInterrupts(). */ - getPendingInterrupts(): BoundInterrupts { - return this.interruptManager.getInterrupts() as BoundInterrupts< - TTools, - TInterrupts - > - } - resolveInterrupts(approved: boolean): void resolveInterrupts( resolver: ( @@ -1755,14 +1730,6 @@ export class ChatClient< return this.streamResponse() } - /** @deprecated Use bound interrupt methods or resumeInterruptsUnsafe(). */ - resumeInterrupts( - resume: Array, - state?: ChatResumeState, - ): Promise { - return this.resumeInterruptsUnsafe(resume, state) - } - private async submitInterruptBatch( submission: InterruptManagerSubmission, ): Promise { @@ -2431,7 +2398,7 @@ export class ChatClient< * @param sendOptions - Per-call overrides. `{ whenBusy }` overrides the * queue policy for this one send. `{ body }` * shallow-merges with `body` and with the chat-level - * `body` / `forwardedProps`. `sendOptions.body` wins + * `forwardedProps`. `sendOptions.body` wins * on key collisions. Framework hooks forward this * object as their second argument. * @@ -2483,7 +2450,7 @@ export class ChatClient< } if (this.hasPendingInterrupts()) { throw new Error( - 'ChatClient: cannot send normal input while pending interrupts exist. Use resumeInterrupts() instead.', + 'ChatClient: cannot send normal input while pending interrupts exist. Resolve the interrupts first.', ) } @@ -2640,7 +2607,7 @@ export class ChatClient< this.mountDevtools() if (this.hasPendingInterrupts()) { throw new Error( - 'ChatClient: cannot append normal input while pending interrupts exist. Use resumeInterrupts() instead.', + 'ChatClient: cannot append normal input while pending interrupts exist. Resolve the interrupts first.', ) } // Normalize the message to ensure it has id and createdAt @@ -2746,17 +2713,14 @@ export class ChatClient< return false } - // Merge sources for the wire `forwardedProps` field, in priority - // order (later spreads win): - // 1. Legacy `body` option (deprecated). - // 2. Canonical `forwardedProps` option (wins over `body`). - // 3. Per-call body (`pendingMessageBody`: positional + sendOptions.body). + // Merge sources for the wire `forwardedProps` field (later spreads win): + // 1. The `forwardedProps` option. + // 2. Per-call body (`pendingMessageBody`: positional + sendOptions.body). // The AG-UI standard `threadId` is sent at the wire's top level for // run/conversation correlation, so we no longer auto-emit a separate // `conversationId` here — `chat({ threadId })` server-side covers the // same role for devtools/observability. const mergedBody = { - ...this.bodyOption, ...this.forwardedPropsOption, ...this.pendingMessageBody, } @@ -3123,8 +3087,8 @@ export class ChatClient< clientTool?.outputSchema && isStandardSchema(clientTool.outputSchema) ? await this.validateClientToolOutput(clientTool, result.output) : result.output - // Only an interrupt resume needs JSON output. The legacy continuation - // keeps the raw value. + // Only an interrupt resume needs JSON output. The message-based + // continuation keeps the raw value. if ( this.interruptManager .getDescriptors() @@ -3176,7 +3140,8 @@ export class ChatClient< ) if (resolvedViaInterrupt) { // Interrupt manager stages/submits the resume batch (deferred until the - // parent stream settles when still loading). Skip legacy continuation. + // parent stream settles when still loading). Skip the message-based + // continuation. return } @@ -3209,64 +3174,6 @@ export class ChatClient< return validation.data } - /** - * Respond to a tool approval request - */ - async addToolApprovalResponse(response: { - id: string // approval.id, not toolCallId - approved: boolean - }): Promise { - // Reflect the decision on the tool-call part so approval UIs that render - // from `part.state` (the deprecated pre-interrupt pattern) clear the prompt - // and show the response. The bound interrupt resolution below drives the - // actual continuation; this keeps the legacy message-state surface in sync. - this.processor.addToolApprovalResponse(response.id, response.approved) - this.devtoolsBridge.emitSnapshot() - - if ( - this.interruptManager.resolveToolApprovalDecision( - response.id, - response.approved, - ) - ) { - return - } - // Find the tool call ID from the approval ID - const messages = this.processor.getMessages() - let foundToolCallId: string | undefined - - for (const msg of messages) { - const toolCallPart = msg.parts.find( - (p: MessagePart): p is ToolCallPart => - p.type === 'tool-call' && p.approval?.id === response.id, - ) - if (toolCallPart) { - foundToolCallId = toolCallPart.id - break - } - } - - if (foundToolCallId) { - this.events.toolApprovalResponded( - response.id, - foundToolCallId, - response.approved, - ) - } - - // Add response via processor - this.processor.addToolApprovalResponse(response.id, response.approved) - this.devtoolsBridge.emitSnapshot() - - // If stream is in progress, queue continuation check for after it ends - if (this.isLoading) { - this.queuePostStreamAction(() => this.checkForContinuation()) - return - } - - await this.checkForContinuation() - } - /** * Queue an action to be executed after the current stream ends */ @@ -3343,7 +3250,7 @@ export class ChatClient< // Ownership follows the descriptors, not the submission handle. Generic // interrupts settle the resume stream through a post-stream action that // runs before `submitInterruptBatch`'s `finally` clears the handle, so - // gating on the handle alone would strand a legacy client tool that the + // gating on the handle alone would strand a frontend tool call that the // native resume itself emitted (#1106). if (this.activeInterruptSubmission && this.hasPendingInterrupts()) { return false @@ -3777,13 +3684,8 @@ export class ChatClient< this.subscribe() } } - // Replace each wire-payload slot independently so callers can update one - // without wiping the other. Passing `undefined` for `body` or - // `forwardedProps` leaves that slot unchanged; context is cleared when the - // key is present with an `undefined` value. - if (options.body !== undefined) { - this.bodyOption = options.body - } + // Passing `undefined` for `forwardedProps` leaves it unchanged; context + // is cleared when the key is present with an `undefined` value. if (options.forwardedProps !== undefined) { this.forwardedPropsOption = options.forwardedProps } diff --git a/packages/ai-client/src/client-persistor.ts b/packages/ai-client/src/client-persistor.ts index 127acbdd49..f2035e046d 100644 --- a/packages/ai-client/src/client-persistor.ts +++ b/packages/ai-client/src/client-persistor.ts @@ -7,11 +7,10 @@ import type { UIMessage, } from './types' -/** Normalize a raw `getItem` result (legacy bare array or combined record). */ +/** Validate a raw `getItem` result. */ function normalizePersistedState( - raw: ChatPersistedState | Array | null | undefined, + raw: ChatPersistedState | null | undefined, ): ChatPersistedState | undefined { - if (Array.isArray(raw)) return { messages: raw } if (raw && Array.isArray(raw.messages)) return raw return undefined } diff --git a/packages/ai-client/src/connection-adapters.ts b/packages/ai-client/src/connection-adapters.ts index 5c8e39ca59..1c8f49ad1d 100644 --- a/packages/ai-client/src/connection-adapters.ts +++ b/packages/ai-client/src/connection-adapters.ts @@ -2,7 +2,6 @@ import { EventType, getChunkRunId as getNormalizedChunkRunId, restoreInboundChunk, - tanstackMetadata, uiMessagesToWire, withTanstackMetadata, } from '@tanstack/ai/client' @@ -335,8 +334,7 @@ async function* readStreamLines( for (const line of lines) { // Strip a trailing CR so a CRLF stream matches the LF path (and the - // XHR reader). Without this an exact-equality check like the `[DONE]` - // sentinel in linesToSSEEvents would miss `data: [DONE]\r`. + // XHR reader). const normalized = line.endsWith('\r') ? line.slice(0, -1) : line if (normalized.trim()) { yield normalized @@ -433,17 +431,6 @@ function restoreInboundUsage(chunk: StreamChunk): StreamChunk { return restoreInboundChunk(chunk) } -function sseChunkModel(chunk: StreamChunk): string | undefined { - const tanstackModel = tanstackMetadata(chunk)?.model - if (typeof tanstackModel === 'string') return tanstackModel - const usage = 'usage' in chunk ? chunk.usage : undefined - if (Array.isArray(usage)) { - const model = (usage[0] as { model?: unknown } | undefined)?.model - if (typeof model === 'string') return model - } - return undefined -} - /** * Parse SSE-format lines into stream events, pairing each chunk with the `id:` * offset of the event it arrived on. Shared by the fetch- and XHR-backed SSE @@ -451,20 +438,13 @@ function sseChunkModel(chunk: StreamChunk): string | undefined { * * Accepts either `data: {...}` lines or bare JSON lines. Skips comments * starting with `:` (proxies and CDNs inject these as keepalives) and the - * `event:` / `retry:` SSE control fields. A `[DONE]` sentinel is treated as a - * terminal event: a synthesized RUN_FINISHED is yielded using the most recent - * upstream `threadId` / `runId` (falling back to `fallbackIds`), so the - * consumer sees a clean terminal event with real correlation ids. + * `event:` / `retry:` SSE control fields. * * A JSON parse failure throws — the consumer surfaces it as an error. */ async function* linesToSSEEvents( lines: AsyncIterable, - fallbackIds?: { threadId?: string; runId?: string }, ): AsyncGenerator { - let lastThreadId: string | undefined - let lastRunId: string | undefined - let lastModel: string | undefined let pendingId: string | undefined for await (const line of lines) { if (line === 'id' || line.startsWith('id:')) { @@ -489,32 +469,7 @@ async function* linesToSSEEvents( continue } const data = parseSseDataLine(line) - if (data === '[DONE]') { - yield { - chunk: withTanstackMetadata( - { - type: EventType.RUN_FINISHED, - threadId: lastThreadId ?? fallbackIds?.threadId ?? '', - runId: lastRunId ?? fallbackIds?.runId ?? '', - timestamp: Date.now(), - }, - { - finishReason: 'stop', - ...(lastModel !== undefined ? { model: lastModel } : {}), - }, - ) as StreamChunk, - } - return - } const chunk = restoreInboundUsage(JSON.parse(data) as StreamChunk) - if ('threadId' in chunk && typeof chunk.threadId === 'string') { - lastThreadId = chunk.threadId - } - if ('runId' in chunk && typeof chunk.runId === 'string') { - lastRunId = chunk.runId - } - const model = sseChunkModel(chunk) - if (model !== undefined) lastModel = model const id = pendingId pendingId = undefined yield { chunk, ...(id !== undefined ? { id } : {}) } @@ -716,11 +671,10 @@ async function fetchGenerationHydration( async function* responseToSSEEvents( response: Response, abortSignal?: AbortSignal, - fallbackIds?: { threadId?: string; runId?: string }, ): AsyncGenerator { await assertResponseOk(response) const reader = getResponseStreamReader(response) - yield* linesToSSEEvents(readStreamLines(reader, abortSignal), fallbackIds) + yield* linesToSSEEvents(readStreamLines(reader, abortSignal)) } /** Yield NDJSON stream events (chunk + offset) from a fetch Response body. */ @@ -1147,7 +1101,7 @@ export function normalizeConnectionAdapter( ) } - // Legacy connect() wrapper + // connect() wrapper let activeBuffer: Array = [] let activeWaiters: Array<(chunk: StreamChunk | null) => void> = [] let activeSubscriber: typeof activeWaiters | undefined @@ -1384,8 +1338,6 @@ function buildRunAgentInputBody( tools: runContext?.clientTools ?? [], context: [], forwardedProps, - // Backward-compat mirror of `forwardedProps` under the legacy field name. - data: { ...forwardedProps }, } } @@ -1481,18 +1433,7 @@ export function fetchServerSentEvents( body: JSON.stringify(requestBody), credentials: resolvedOptions.credentials || 'same-origin', }, - // Thread the run's ids so a `[DONE]`-terminating server that doesn't - // stamp them onto events still yields a correlated terminal (parity - // with the XHR adapter's xhrSSEParser). - (response, sseSignal) => - responseToSSEEvents(response, sseSignal, { - ...(runContext?.threadId !== undefined - ? { threadId: runContext.threadId } - : {}), - ...(runContext?.runId !== undefined - ? { runId: runContext.runId } - : {}), - }), + responseToSSEEvents, ), signal, resolvedOptions.reconnect, @@ -1526,9 +1467,7 @@ export function fetchServerSentEvents( headers: requestHeaders, credentials: resolvedOptions.credentials || 'same-origin', }, - // A `[DONE]` during a join correlates to the joined run id. - (response, sseSignal) => - responseToSSEEvents(response, sseSignal, { runId }), + responseToSSEEvents, ), signal, resolvedOptions.reconnect, @@ -2210,17 +2149,6 @@ function xhrEventSource( } } -/** SSE line parser bound to the run's ids for a `[DONE]` fallback. */ -function xhrSSEParser(runContext: RunAgentInputContext | undefined) { - const fallbackIds: { threadId?: string; runId?: string } = { - ...(runContext?.threadId !== undefined - ? { threadId: runContext.threadId } - : {}), - ...(runContext?.runId !== undefined ? { runId: runContext.runId } : {}), - } - return (lines: AsyncIterable) => linesToSSEEvents(lines, fallbackIds) -} - /** * Create an XMLHttpRequest-backed Server-Sent Events connection adapter. * @@ -2250,7 +2178,7 @@ export function xhrServerSentEvents( messages, data, runContext, - xhrSSEParser(runContext), + linesToSSEEvents, ), signal, resolvedOptions.reconnect, @@ -2269,9 +2197,7 @@ export function xhrServerSentEvents( [], undefined, undefined, - // A `[DONE]` during a join correlates to the joined run id (parity - // with fetchServerSentEvents.joinRun). - (lines) => linesToSSEEvents(lines, { runId }), + linesToSSEEvents, ), signal, resolvedOptions.reconnect, diff --git a/packages/ai-client/src/devtools.ts b/packages/ai-client/src/devtools.ts index e98de1ae54..cb3681b00d 100644 --- a/packages/ai-client/src/devtools.ts +++ b/packages/ai-client/src/devtools.ts @@ -2170,21 +2170,6 @@ class ChatDevtoolsAwareEventEmitter extends DefaultChatClientEventEmitter { this.afterEmit() } - override toolApprovalResponded( - approvalId: string, - toolCallId: string, - approved: boolean, - context?: ChatClientRunEventContext, - ): void { - super.toolApprovalResponded( - approvalId, - toolCallId, - approved, - context ?? this.helper.getCurrentRunEventContext(), - ) - this.afterEmit() - } - // -- methods without context (just auto-emit snapshot) ------------------- override clientCreated(initialMessageCount: number): void { diff --git a/packages/ai-client/src/events.ts b/packages/ai-client/src/events.ts index 206493a50d..735f2f2d88 100644 --- a/packages/ai-client/src/events.ts +++ b/packages/ai-client/src/events.ts @@ -294,23 +294,6 @@ export abstract class ChatClientEventEmitter { }) } - /** - * Emit tool approval responded event - */ - toolApprovalResponded( - approvalId: string, - toolCallId: string, - approved: boolean, - context?: ChatClientRunEventContext, - ): void { - this.emitEvent('tools:approval:responded', { - approvalId, - toolCallId, - approved, - ...context, - }) - } - /** * Emit tool fixture applied event. */ diff --git a/packages/ai-client/src/index.ts b/packages/ai-client/src/index.ts index 15376535c2..f446e43540 100644 --- a/packages/ai-client/src/index.ts +++ b/packages/ai-client/src/index.ts @@ -159,8 +159,6 @@ export type { RunFinishedOutcome, } from '@tanstack/ai/client' export type { - RealtimeAdapter, - RealtimeConnection, RealtimeClientOptions, RealtimeClientState, RealtimeStateChangeCallback, diff --git a/packages/ai-client/src/interrupt-manager.ts b/packages/ai-client/src/interrupt-manager.ts index 04d5e8475e..648e81537b 100644 --- a/packages/ai-client/src/interrupt-manager.ts +++ b/packages/ai-client/src/interrupt-manager.ts @@ -152,7 +152,6 @@ const itemErrorCodes = new Set([ 'expired', 'stale', 'conflict', - 'legacy-unsupported', ]) const batchErrorCodes = new Set([ @@ -169,44 +168,12 @@ const batchErrorCodes = new Set([ 'expired', 'stale', 'conflict', - 'legacy-submit-failed', ]) function isUnknownObject(value: unknown): value is UnknownObject { return value !== null && typeof value === 'object' && !Array.isArray(value) } -function isLegacyApprovalMetadata(value: unknown): boolean { - return ( - isUnknownObject(value) && - value['kind'] === 'approval' && - typeof value['toolName'] === 'string' && - 'input' in value - ) -} - -function isLegacyClientToolMetadata(value: unknown): boolean { - return ( - isUnknownObject(value) && - value['kind'] === 'client_tool' && - typeof value['toolName'] === 'string' && - 'input' in value - ) -} - -/** - * Does this descriptor carry the pre-binding TanStack metadata marker? - * - * Descriptors emitted before the resume binding existed are still ours to - * resume, so they must not be mistaken for another producer's interrupt. - */ -function isLegacyInterruptMetadata(interrupt: Interrupt): boolean { - return ( - isLegacyApprovalMetadata(interrupt.metadata) || - isLegacyClientToolMetadata(interrupt.metadata) - ) -} - function getDescriptorBinding( interrupt: Interrupt, ): InterruptBinding | undefined { @@ -250,17 +217,6 @@ function getInterruptPayload(interrupt: Interrupt): unknown { return interrupt.metadata?.['tanstack:interruptPayload'] } -/** - * Only used to route *legacy* (pre-binding) descriptors, which have no binding - * to classify off. Current descriptors are classified by their binding alone. - */ -function isClientToolExecutionReason(reason: string): boolean { - return ( - reason === 'tanstack:client_tool_execution' || - reason === 'client_tool_input' - ) -} - function responseSchemaHash(interrupt: Interrupt): string | undefined { if (interrupt.responseSchema === undefined) return undefined try { @@ -472,7 +428,6 @@ function baseSnapshot( item.error === undefined ? Object.freeze([]) : Object.freeze([cloneAndDeepFreezeJson(item.error)]) - const error = errors[0] return { id: descriptor.id, interruptId: descriptor.id, @@ -494,7 +449,6 @@ function baseSnapshot( generation: hydration.generation, status: item.status, errors, - ...(error !== undefined ? { error } : {}), canResolve: item.canResolve, } } @@ -513,7 +467,6 @@ export class InterruptManager< Object.freeze([]) private state: ChatInterruptState = Object.freeze({ interrupts: this.snapshot, - pendingInterrupts: this.snapshot, interruptErrors: this.rootErrors, resuming: false, }) @@ -618,7 +571,6 @@ export class InterruptManager< this.resuming = false this.state = Object.freeze({ interrupts: this.snapshot, - pendingInterrupts: this.snapshot, interruptErrors: this.rootErrors, resuming: false, }) @@ -708,24 +660,11 @@ export class InterruptManager< ): boolean { const item = this.items.find( (candidate) => - (candidate.kind === 'client-tool-execution' && - candidate.binding?.kind === 'client-tool-execution' && - candidate.binding.toolCallId === toolCallId) || - (candidate.kind === 'generic' && - isClientToolExecutionReason(candidate.descriptor.reason) && - candidate.descriptor.toolCallId === toolCallId && - isLegacyClientToolMetadata(candidate.descriptor.metadata)), + candidate.kind === 'client-tool-execution' && + candidate.binding?.kind === 'client-tool-execution' && + candidate.binding.toolCallId === toolCallId, ) if (!item) return false - if (item.kind !== 'client-tool-execution') { - this.resolveItem( - item.descriptor.id, - result.state === 'output-error' - ? { error: result.errorText } - : result.output, - ) - return true - } if (result.state === 'output-available') { this.resolveItem(item.descriptor.id, result.output) return true @@ -767,20 +706,6 @@ export class InterruptManager< this.maybeSubmit() } - resolveToolApprovalDecision(interruptId: string, approved: boolean): boolean { - const item = this.items.find( - (candidate) => - candidate.descriptor.id === interruptId && - (candidate.kind === 'tool-approval' || - (candidate.kind === 'generic' && - candidate.descriptor.reason === 'approval_required' && - isLegacyApprovalMetadata(candidate.descriptor.metadata))), - ) - if (!item) return false - this.resolveItem(item.descriptor.id, { approved }) - return true - } - private hydrateInterrupt( descriptor: Interrupt, hydration: InterruptManagerHydration, @@ -788,8 +713,6 @@ export class InterruptManager< ): RuntimeInterrupt { const interrupt = cloneAndDeepFreezeJson(descriptor) const candidate = getDescriptorBinding(interrupt) - const legacyResumable = - candidate === undefined && isLegacyInterruptMetadata(interrupt) // No binding we understand, and nothing else identifying the descriptor as // ours, means this interrupt was not produced by this package's resume @@ -803,11 +726,7 @@ export class InterruptManager< // the user has already filled it in. Surface it as unresolvable instead, // so "someone else owns this pause" is visible rather than silently // translated into an AI-domain interrupt. - // - // Pre-binding TanStack descriptors are still ours: they carry the legacy - // `metadata.kind` marker, so they keep hydrating through the generic path - // below. - if (candidate === undefined && !legacyResumable) { + if (candidate === undefined) { if (hasReservedFirstPartyBindingMarker(interrupt)) { return { descriptor: interrupt, @@ -836,14 +755,12 @@ export class InterruptManager< } const correlated = - candidate !== undefined && candidate.interruptId === interrupt.id && candidate.interruptedRunId === hydration.interruptedRunId && candidate.generation === hydration.generation && responseSchemaHash(interrupt) === candidate.responseSchemaHash const structurallyCorrelated = - candidate !== undefined && candidate.interruptId === interrupt.id && candidate.interruptedRunId === hydration.interruptedRunId && candidate.generation === hydration.generation && @@ -851,7 +768,6 @@ export class InterruptManager< (responseSchemaHash(interrupt) ?? candidate.responseSchemaHash) if ( - candidate !== undefined && hasFirstPartyGenericMarker(interrupt) && (!structurallyCorrelated || candidate.kind !== 'generic' || @@ -937,7 +853,6 @@ export class InterruptManager< } if ( - candidate !== undefined && candidate.kind === 'generic' && candidate.definitionId !== undefined && candidate.key !== undefined && @@ -1003,14 +918,12 @@ export class InterruptManager< } const resumable = - legacyResumable || - (candidate !== undefined && - candidate.interruptId === interrupt.id && - candidate.interruptedRunId === hydration.interruptedRunId && - candidate.generation === hydration.generation && - (candidate.kind !== 'generic' || - responseSchemaHash(interrupt) === undefined || - candidate.responseSchemaHash === responseSchemaHash(interrupt))) + candidate.interruptId === interrupt.id && + candidate.interruptedRunId === hydration.interruptedRunId && + candidate.generation === hydration.generation && + (candidate.kind !== 'generic' || + responseSchemaHash(interrupt) === undefined || + candidate.responseSchemaHash === responseSchemaHash(interrupt)) return { descriptor: interrupt, binding: genericBinding(interrupt, hydration, candidate), @@ -1147,7 +1060,6 @@ export class InterruptManager< this.snapshot = Object.freeze([]) this.state = Object.freeze({ interrupts: this.snapshot, - pendingInterrupts: this.snapshot, interruptErrors: this.rootErrors, resuming: this.resuming, }) @@ -1157,7 +1069,6 @@ export class InterruptManager< this.snapshot = this.buildSnapshot() this.state = Object.freeze({ interrupts: this.snapshot, - pendingInterrupts: this.snapshot, interruptErrors: this.rootErrors, resuming: this.resuming, }) diff --git a/packages/ai-client/src/realtime-client.ts b/packages/ai-client/src/realtime-client.ts index 298fb7133d..0bac78f317 100644 --- a/packages/ai-client/src/realtime-client.ts +++ b/packages/ai-client/src/realtime-client.ts @@ -9,6 +9,7 @@ import type { Atom } from './snapshot-atom' import type { AnyClientTool, AudioVisualization, + RealtimeConnection, RealtimeMessage, RealtimeMode, RealtimeSessionConfig, @@ -18,7 +19,6 @@ import type { import type { RealtimeClientOptions, RealtimeClientState, - RealtimeConnection, RealtimeStateChangeCallback, } from './realtime-types' diff --git a/packages/ai-client/src/realtime-types.ts b/packages/ai-client/src/realtime-types.ts index 235190dc53..ab17f954a6 100644 --- a/packages/ai-client/src/realtime-types.ts +++ b/packages/ai-client/src/realtime-types.ts @@ -8,12 +8,6 @@ import type { } from '@tanstack/ai/client' import type { UsageInfo } from '@tanstack/ai' -// The realtime adapter contract lives in `@tanstack/ai` (the shared layer both -// providers and this client depend on) so provider packages don't need to -// depend on `@tanstack/ai-client`. Re-exported here for backwards compatibility -// — `import { RealtimeAdapter } from '@tanstack/ai-client'` keeps working. -export type { RealtimeAdapter, RealtimeConnection } from '@tanstack/ai/client' - // ============================================================================ // Client Options // ============================================================================ diff --git a/packages/ai-client/src/sse-parser.ts b/packages/ai-client/src/sse-parser.ts index 0c8a70a159..620b28cc06 100644 --- a/packages/ai-client/src/sse-parser.ts +++ b/packages/ai-client/src/sse-parser.ts @@ -77,13 +77,6 @@ export async function* parseSSEResponse( for await (const line of readStreamLines(reader, abortSignal)) { const data = parseSseDataLine(line) - if (data === '[DONE]') { - console.warn( - '[@tanstack/ai-client] Received [DONE] sentinel. This is deprecated — upgrade your @tanstack/ai server package. RUN_FINISHED is the stream terminator.', - ) - continue - } - try { const parsed: StreamChunk = JSON.parse(data) yield parsed diff --git a/packages/ai-client/src/types.ts b/packages/ai-client/src/types.ts index 0030286806..5582202eb2 100644 --- a/packages/ai-client/src/types.ts +++ b/packages/ai-client/src/types.ts @@ -74,8 +74,6 @@ export interface BoundInterruptBase { readonly generation: number readonly status: InterruptItemStatus readonly errors: ReadonlyArray - /** @deprecated Use `errors[0]`. */ - readonly error?: ItemInterruptError /** * Whether the binding/schema allows resolution at hydrate time. * Does not flip on submit/expiry — gate UI on `status`, `resuming`, and @@ -271,15 +269,13 @@ export interface ChatInterruptState< readonly [], > { readonly interrupts: BoundInterrupts - /** @deprecated Use `interrupts`. Same snapshot today. */ - readonly pendingInterrupts: BoundInterrupts readonly interruptErrors: ReadonlyArray readonly resuming: boolean } /** * `messages` is the full UIMessage history (not a delta). `data` is the - * merged body — `ChatClientOptions.body` plus any per-call data passed to + * merged body — `ChatClientOptions.forwardedProps` plus any per-call data passed to * `sendMessage(...)`. `threadId` / `runId` are the AG-UI correlation ids * the chat client uses to track this turn — forward them to your server * if it needs to correlate requests. @@ -848,10 +844,6 @@ export interface ChatPersistedState< * message transcript and the resume snapshot as one {@link ChatPersistedState} * record, so a full page reload restores the conversation AND can rejoin an * in-flight run / rehydrate pending interrupts. - * - * For backward compatibility `getItem` may also return a bare `UIMessage[]` - * (the legacy messages-only format); the client normalizes it to - * `{ messages }`. `setItem` always writes the combined record. */ export interface ChatClientPersistence< TTools extends ReadonlyArray = any, @@ -860,12 +852,9 @@ export interface ChatClientPersistence< id: string, ) => | ChatPersistedState - | Array> | null | undefined - | Promise< - ChatPersistedState | Array> | null | undefined - > + | Promise | null | undefined> setItem: ( id: string, state: ChatPersistedState, @@ -1034,21 +1023,9 @@ export interface ChatClientBaseOptions< * AG-UI `RunAgentInput.forwardedProps` field. Use this for per-session * options like provider/model selection or feature flags that the * server endpoint should read. - * - * Replaces the legacy `body` option. If both are provided, - * `forwardedProps` wins on key collision. */ forwardedProps?: Record - /** - * @deprecated Use `forwardedProps` instead. `body` continues to work - * unchanged — its values are merged into the AG-UI - * `RunAgentInput.forwardedProps` field on the wire and are also - * mirrored under the legacy `data` field for servers that have not - * migrated yet. Will be removed in a future major release. - */ - body?: Record - /** * Optional BYOK keyring. On each send the client prepares the resolved * provider and stamps `x-byok-*` request headers. Keys never go in the body. diff --git a/packages/ai-client/tests/chat-client-context.test.ts b/packages/ai-client/tests/chat-client-context.test.ts index b5be4df41a..5a757ccb8a 100644 --- a/packages/ai-client/tests/chat-client-context.test.ts +++ b/packages/ai-client/tests/chat-client-context.test.ts @@ -107,20 +107,23 @@ describe('ChatClient runtime context', () => { it('clears client-local context when updateOptions receives undefined', async () => { type ClientContext = { localUserId: string } - const toolChunks = createToolCallChunks([ - { - id: 'tc-update-context', - name: 'read_optional_context', - arguments: '{}', - }, - ]) const textChunks = createTextChunks('done', 'msg-update-context') const outputs: Array = [] let callIndex = 0 const adapter: ConnectConnectionAdapter = { async *connect(_messages, _data, abortSignal) { - const chunks = callIndex % 2 === 0 ? toolChunks : textChunks + // A new call id per turn: an answered call does not run again. + const chunks = + callIndex % 2 === 0 + ? createToolCallChunks([ + { + id: `tc-update-context-${callIndex}`, + name: 'read_optional_context', + arguments: '{}', + }, + ]) + : textChunks callIndex++ for (const chunk of chunks) { if (abortSignal?.aborted) { diff --git a/packages/ai-client/tests/chat-client-interrupts.test.ts b/packages/ai-client/tests/chat-client-interrupts.test.ts index 601eea89ca..7181c4de46 100644 --- a/packages/ai-client/tests/chat-client-interrupts.test.ts +++ b/packages/ai-client/tests/chat-client-interrupts.test.ts @@ -1200,39 +1200,6 @@ describe('InterruptManager transactions', () => { ).toBe(true) }) - it('resolves pre-binding client-tool fallback with raw output', () => { - const { manager, submit } = createManager() - manager.hydrate({ - threadId: 'thread-1', - interruptedRunId: 'run-1', - generation: 1, - interrupts: [ - { - id: 'client-degraded', - reason: 'tanstack:client_tool_execution', - toolCallId: 'call-degraded', - metadata: { - kind: 'client_tool', - toolName: 'lookup', - input: {}, - }, - }, - ], - }) - // Schema drift / missing tool → public generic item - expect(manager.getInterrupts()[0]?.kind).toBe('generic') - expect( - manager.resolveClientToolOutput('call-degraded', { accountId: 'a' }), - ).toBe(true) - expect(submit.mock.calls[0]?.[0].resolutions).toEqual([ - { - interruptId: 'client-degraded', - status: 'resolved', - payload: { accountId: 'a' }, - }, - ]) - }) - it('supersedes a server batch error set without dropping local client, transport, or item errors', async () => { const firstErrors: ReadonlyArray = [ { @@ -1493,8 +1460,6 @@ describe('ChatClient native interrupts', () => { client.getInterruptState(), { source: 'hydrate' }, ) - const state = onInterruptStateChange.mock.lastCall?.[0] - expect(state?.interrupts).toBe(state?.pendingInterrupts) }) it('reports source hydrate when a client-tool interrupt is restored', () => { @@ -1619,15 +1584,16 @@ describe('ChatClient native interrupts', () => { delta: '{}', timestamp: Date.now(), } - yield { - type: EventType.CUSTOM, - name: 'tool-input-available', - value: { - toolCallId: 'call-1', - toolName: 'lookup', - input: {}, - }, - timestamp: Date.now(), + const binding: InterruptBinding = { + v: INTERRUPT_BINDING_VERSION, + kind: 'client-tool-execution', + interruptId: 'client-1', + interruptedRunId: runId, + generation: 0, + toolName: 'lookup', + toolCallId: 'call-1', + outputSchemaHash, + responseSchemaHash, } yield { type: EventType.RUN_FINISHED, @@ -1637,20 +1603,15 @@ describe('ChatClient native interrupts', () => { outcome: { type: 'interrupt', interrupts: [ - descriptor( - { - v: INTERRUPT_BINDING_VERSION, - kind: 'client-tool-execution', - interruptId: 'client-1', - interruptedRunId: runId, - generation: 0, + descriptor(binding, { + responseSchema, + metadata: { + kind: 'client_tool', toolName: 'lookup', - toolCallId: 'call-1', - outputSchemaHash, - responseSchemaHash, + input: {}, + 'tanstack:interruptBinding': binding, }, - { responseSchema }, - ), + }), ], }, } @@ -1796,9 +1757,7 @@ describe('ChatClient native interrupts', () => { ) const state = client.getInterruptState() expect(Object.isFrozen(state)).toBe(true) - expect(state.interrupts).toBe(state.pendingInterrupts) expect(client.getInterrupts()).toBe(state.interrupts) - expect(client.getPendingInterrupts()).toBe(state.interrupts) const item = state.interrupts[0] if (item?.kind !== 'generic') throw new Error('Expected generic interrupt') item.resolveInterrupt({ answer: 42 }) diff --git a/packages/ai-client/tests/chat-client-reasoning-rejoin.test.ts b/packages/ai-client/tests/chat-client-reasoning-rejoin.test.ts index 0ca7689281..e6ef5351fc 100644 --- a/packages/ai-client/tests/chat-client-reasoning-rejoin.test.ts +++ b/packages/ai-client/tests/chat-client-reasoning-rejoin.test.ts @@ -34,17 +34,6 @@ const firstEvents: Array<{ ], thinking: '', }, - { - name: 'step signature', - chunks: [ - { - type: EventType.STEP_FINISHED, - stepName: 'thinking', - timestamp: 2, - ...{ signature: 'signature' }, - }, - ], - }, ] it.each(firstEvents)( diff --git a/packages/ai-client/tests/chat-client-resume.test.ts b/packages/ai-client/tests/chat-client-resume.test.ts index 0f14c938cb..785f37e9b9 100644 --- a/packages/ai-client/tests/chat-client-resume.test.ts +++ b/packages/ai-client/tests/chat-client-resume.test.ts @@ -73,16 +73,20 @@ async function createInterruptedClient(continuation: Script) { timestamp: Date.now(), outcome: { type: 'interrupt', - // Full legacy approval metadata (kind + toolName + input) hydrates as - // a public generic interrupt so resolveInterrupt() can drive resume. + // A bound generic interrupt so resolveInterrupt() can drive resume. interrupts: [ { id: 'interrupt-1', - reason: 'approval_required', + reason: 'confirmation', metadata: { - kind: 'approval', - toolName: 'confirm', - input: {}, + 'tanstack:interruptBinding': { + v: INTERRUPT_BINDING_VERSION, + kind: 'generic', + interruptId: 'interrupt-1', + interruptedRunId: ctx?.runId ?? 'interrupted-run', + generation: 0, + responseSchemaHash: 'none', + }, }, }, ], @@ -247,12 +251,12 @@ describe('ChatClient resume', () => { threadId: 'thread-1', runId: expect.any(String), }) - expect(client.getPendingInterrupts()).toEqual([ + expect(client.getInterrupts()).toEqual([ expect.objectContaining({ id: 'interrupt-1' }), ]) }) - it('resumeInterrupts sends AG-UI resume entries with the interrupted run context', async () => { + it('resumeInterruptsUnsafe sends AG-UI resume entries with the interrupted run context', async () => { const resumeItems: Array = [ { interruptId: 'interrupt-1', @@ -300,13 +304,13 @@ describe('ChatClient resume', () => { await client.sendMessage('hi') const resumeState = client.getResumeState() - await client.resumeInterrupts(resumeItems) + await client.resumeInterruptsUnsafe(resumeItems) expect(contexts[1]?.threadId).toBe(resumeState?.threadId) expect(contexts[1]?.runId).not.toBe(resumeState?.runId) expect(contexts[1]?.parentRunId).toBe(resumeState?.runId) expect(contexts[1]?.resume).toEqual(resumeItems) - expect(client.getPendingInterrupts()).toEqual([]) + expect(client.getInterrupts()).toEqual([]) expect(client.getResumeState()).toBeNull() }) @@ -356,13 +360,13 @@ describe('ChatClient resume', () => { const client = new ChatClient({ connection: adapter, threadId: 'thread-1' }) await client.sendMessage('hi') - expect(client.getPendingInterrupts()).toHaveLength(1) + expect(client.getInterrupts()).toHaveLength(1) - await client.resumeInterrupts(resumeItems) + await client.resumeInterruptsUnsafe(resumeItems) expect(contexts[1]?.runId).not.toBe('provider-continuation-run') expect(client.getResumeState()).toBeNull() - expect(client.getPendingInterrupts()).toEqual([]) + expect(client.getInterrupts()).toEqual([]) }) it('correlates a synthesized resume finish to the client request run', async () => { @@ -408,7 +412,7 @@ describe('ChatClient resume', () => { }) }) - it('resumeInterrupts reconnects with the full current message history', async () => { + it('resumeInterruptsUnsafe reconnects with the full current message history', async () => { const resumeItems: Array = [ { interruptId: 'interrupt-1', @@ -443,7 +447,7 @@ describe('ChatClient resume', () => { await client.sendMessage('hi') expect(sentMessages[0]).toHaveLength(1) - await client.resumeInterrupts(resumeItems) + await client.resumeInterruptsUnsafe(resumeItems) expect(contexts[1]?.resume).toEqual(resumeItems) expect(sentMessages[1]).not.toEqual([]) @@ -484,7 +488,7 @@ describe('ChatClient resume', () => { const client = new ChatClient({ connection: adapter, threadId: 'thread-1' }) await client.sendMessage('hi') - expect(client.getPendingInterrupts()).toHaveLength(1) + expect(client.getInterrupts()).toHaveLength(1) ;( client as unknown as { observeInterruptState: (chunk: StreamChunk) => void @@ -496,7 +500,7 @@ describe('ChatClient resume', () => { }) expect(client.getResumeState()).toBeNull() - expect(client.getPendingInterrupts()).toEqual([]) + expect(client.getInterrupts()).toEqual([]) await expect(client.sendMessage('fresh')).resolves.toBeUndefined() }) @@ -565,7 +569,7 @@ describe('ChatClient resume', () => { const client = new ChatClient({ connection: adapter, threadId: 'thread-1' }) await client.sendMessage('hi') - expect(client.getPendingInterrupts()).toHaveLength(1) + expect(client.getInterrupts()).toHaveLength(1) ;( client as unknown as { observeInterruptState: (chunk: StreamChunk) => void @@ -578,7 +582,7 @@ describe('ChatClient resume', () => { outcome: { type: 'success' }, }) - expect(client.getPendingInterrupts()).toEqual([ + expect(client.getInterrupts()).toEqual([ expect.objectContaining({ id: 'interrupt-a' }), ]) expect(client.getResumeState()?.runId).not.toBe('run-b') @@ -614,7 +618,7 @@ describe('ChatClient resume', () => { await client.sendMessage('hi') const interruptedState = client.getResumeState() - expect(client.getPendingInterrupts()).toHaveLength(1) + expect(client.getInterrupts()).toHaveLength(1) const internals = client as unknown as { updateRunLifecycle: (chunk: StreamChunk) => void observeInterruptState: (chunk: StreamChunk) => void @@ -634,7 +638,7 @@ describe('ChatClient resume', () => { }) expect(client.getResumeState()).toEqual(interruptedState) - expect(client.getPendingInterrupts()).toEqual([ + expect(client.getInterrupts()).toEqual([ expect.objectContaining({ id: 'interrupt-a' }), ]) await expect(client.sendMessage('blocked')).rejects.toThrow( @@ -668,7 +672,7 @@ describe('ChatClient resume', () => { const client = new ChatClient({ connection: adapter, threadId: 'thread-1' }) await client.sendMessage('hi') - expect(client.getPendingInterrupts()).toHaveLength(1) + expect(client.getInterrupts()).toHaveLength(1) ;( client as unknown as { observeInterruptState: (chunk: StreamChunk) => void @@ -680,7 +684,7 @@ describe('ChatClient resume', () => { timestamp: Date.now(), }) - expect(client.getPendingInterrupts()).toEqual([ + expect(client.getInterrupts()).toEqual([ expect.objectContaining({ id: 'interrupt-a' }), ]) await expect( @@ -736,7 +740,7 @@ describe('ChatClient resume', () => { ]) const seenResumeStates: Array> = [] const seenPendingInterrupts: Array< - ReturnType + ReturnType > = [] const client = new ChatClient({ connection: adapter, @@ -749,12 +753,12 @@ describe('ChatClient resume', () => { await client.sendMessage('hi') expect(client.getResumeState()).not.toBeNull() - expect(client.getPendingInterrupts()).toHaveLength(1) + expect(client.getInterrupts()).toHaveLength(1) client.clear() expect(client.getResumeState()).toBeNull() - expect(client.getPendingInterrupts()).toEqual([]) + expect(client.getInterrupts()).toEqual([]) expect(seenResumeStates.at(-1)).toBeNull() expect(seenPendingInterrupts.at(-1)).toEqual([]) await expect(client.sendMessage('fresh')).resolves.toBeUndefined() @@ -769,318 +773,6 @@ describe('ChatClient resume', () => { expect(contexts).toHaveLength(3) }) - it('addToolApprovalResponse sends a compatibility resume item', async () => { - const { adapter, contexts } = recordingAdapter([ - (ctx) => [ - { - type: EventType.RUN_STARTED, - runId: ctx?.runId ?? 'run-1', - threadId: ctx?.threadId ?? 'thread-1', - timestamp: Date.now(), - }, - text('a'), - { - type: EventType.RUN_FINISHED, - runId: ctx?.runId ?? 'run-1', - threadId: ctx?.threadId ?? 'thread-1', - timestamp: Date.now(), - outcome: { - type: 'interrupt', - interrupts: [ - { - id: 'approval-1', - reason: 'approval_required', - toolCallId: 'tool-1', - metadata: { - kind: 'approval', - toolName: 'lookup', - input: { query: 'first' }, - }, - }, - ], - }, - }, - ], - [ - { - type: EventType.RUN_FINISHED, - runId: 'run-1', - threadId: 'thread-1', - timestamp: Date.now(), - outcome: { type: 'success' }, - }, - ], - ]) - const client = new ChatClient({ connection: adapter, threadId: 'thread-1' }) - - await client.sendMessage('hi') - await client.addToolApprovalResponse({ id: 'approval-1', approved: true }) - - expect(contexts[1]?.resume).toEqual([ - { - interruptId: 'approval-1', - status: 'resolved', - payload: { approved: true }, - }, - ]) - }) - - it('addToolApprovalResponse waits for all pending approval interrupts before resuming', async () => { - const { adapter, contexts } = recordingAdapter([ - (ctx) => [ - { - type: EventType.RUN_STARTED, - runId: ctx?.runId ?? 'run-1', - threadId: ctx?.threadId ?? 'thread-1', - timestamp: Date.now(), - }, - text('a'), - { - type: EventType.RUN_FINISHED, - runId: ctx?.runId ?? 'run-1', - threadId: ctx?.threadId ?? 'thread-1', - timestamp: Date.now(), - outcome: { - type: 'interrupt', - interrupts: [ - { - id: 'approval-1', - reason: 'approval_required', - toolCallId: 'tool-1', - metadata: { - kind: 'approval', - toolName: 'lookup', - input: { query: 'first' }, - }, - }, - { - id: 'approval-2', - reason: 'approval_required', - toolCallId: 'tool-2', - metadata: { - kind: 'approval', - toolName: 'lookup', - input: { query: 'second' }, - }, - }, - ], - }, - }, - ], - [], - ]) - const client = new ChatClient({ connection: adapter, threadId: 'thread-1' }) - - await client.sendMessage('hi') - await client.addToolApprovalResponse({ id: 'approval-1', approved: true }) - - expect(contexts).toHaveLength(1) - - await client.addToolApprovalResponse({ id: 'approval-2', approved: false }) - - expect(contexts).toHaveLength(2) - expect(contexts[1]?.resume).toEqual([ - { - interruptId: 'approval-1', - status: 'resolved', - payload: { approved: true }, - }, - { - interruptId: 'approval-2', - status: 'resolved', - payload: { approved: false }, - }, - ]) - }) - - it('restores pending interrupts from an initial resume snapshot', async () => { - const { adapter, contexts } = recordingAdapter([ - [ - { - type: EventType.RUN_FINISHED, - runId: 'run-1', - threadId: 'thread-1', - timestamp: Date.now(), - outcome: { type: 'success' }, - }, - ], - ]) - const client = new ChatClient({ - connection: adapter, - initialResumeSnapshot: { - resumeState: { - threadId: 'thread-1', - runId: 'run-1', - }, - pendingInterrupts: [ - { - id: 'approval-1', - reason: 'approval_required', - toolCallId: 'tool-1', - metadata: { - kind: 'approval', - toolName: 'lookup', - input: { query: 'restored' }, - }, - }, - ], - }, - }) - - expect(client.getResumeState()).toEqual({ - threadId: 'thread-1', - runId: 'run-1', - }) - expect(client.getPendingInterrupts()).toEqual([ - expect.objectContaining({ id: 'approval-1' }), - ]) - - await client.addToolApprovalResponse({ id: 'approval-1', approved: true }) - - expect(contexts[0]?.threadId).toBe('thread-1') - expect(contexts[0]?.runId).not.toBe('run-1') - expect(contexts[0]?.parentRunId).toBe('run-1') - expect(contexts[0]?.resume).toEqual([ - { - interruptId: 'approval-1', - status: 'resolved', - payload: { approved: true }, - }, - ]) - }) - - it('addToolResult for pending client-tool input sends a resume item', async () => { - const { adapter, contexts } = recordingAdapter([ - (ctx) => [ - { - type: EventType.RUN_STARTED, - runId: ctx?.runId ?? 'run-1', - threadId: ctx?.threadId ?? 'thread-1', - timestamp: Date.now(), - }, - text('a'), - { - type: EventType.RUN_FINISHED, - runId: ctx?.runId ?? 'run-1', - threadId: ctx?.threadId ?? 'thread-1', - timestamp: Date.now(), - outcome: { - type: 'interrupt', - interrupts: [ - { - id: 'interrupt-tool-1', - reason: 'client_tool_input', - toolCallId: 'tool-call-1', - metadata: { - kind: 'client_tool', - toolName: 'lookup', - input: { query: 'first' }, - }, - }, - ], - }, - }, - ], - [], - ]) - const client = new ChatClient({ connection: adapter, threadId: 'thread-1' }) - - await client.sendMessage('hi') - const resumeState = client.getResumeState() - await client.addToolResult({ - toolCallId: 'tool-call-1', - tool: 'lookup', - output: { answer: 42 }, - }) - - expect(contexts[1]?.threadId).toBe(resumeState?.threadId) - expect(contexts[1]?.runId).not.toBe(resumeState?.runId) - expect(contexts[1]?.parentRunId).toBe(resumeState?.runId) - expect(contexts[1]?.resume).toEqual([ - { - interruptId: 'interrupt-tool-1', - status: 'resolved', - payload: { answer: 42 }, - }, - ]) - }) - - it('addToolResult waits for all pending client-tool interrupts before resuming', async () => { - const { adapter, contexts } = recordingAdapter([ - (ctx) => [ - { - type: EventType.RUN_STARTED, - runId: ctx?.runId ?? 'run-1', - threadId: ctx?.threadId ?? 'thread-1', - timestamp: Date.now(), - }, - text('a'), - { - type: EventType.RUN_FINISHED, - runId: ctx?.runId ?? 'run-1', - threadId: ctx?.threadId ?? 'thread-1', - timestamp: Date.now(), - outcome: { - type: 'interrupt', - interrupts: [ - { - id: 'interrupt-tool-1', - reason: 'client_tool_input', - toolCallId: 'tool-call-1', - metadata: { - kind: 'client_tool', - toolName: 'lookup', - input: { query: 'first' }, - }, - }, - { - id: 'interrupt-tool-2', - reason: 'client_tool_input', - toolCallId: 'tool-call-2', - metadata: { - kind: 'client_tool', - toolName: 'lookup', - input: { query: 'second' }, - }, - }, - ], - }, - }, - ], - [], - ]) - const client = new ChatClient({ connection: adapter, threadId: 'thread-1' }) - - await client.sendMessage('hi') - await client.addToolResult({ - toolCallId: 'tool-call-1', - tool: 'lookup', - output: { answer: 42 }, - }) - - expect(contexts).toHaveLength(1) - - await client.addToolResult({ - toolCallId: 'tool-call-2', - tool: 'lookup', - output: { answer: 43 }, - }) - - expect(contexts).toHaveLength(2) - expect(contexts[1]?.resume).toEqual([ - { - interruptId: 'interrupt-tool-1', - status: 'resolved', - payload: { answer: 42 }, - }, - { - interruptId: 'interrupt-tool-2', - status: 'resolved', - payload: { answer: 43 }, - }, - ]) - }) - it('auto-executed client tool during parent stream defers resume until isLoading clears', async () => { const outputSchema = z.object({ answer: z.number() }) const lookup = toolDefinition({ @@ -1320,7 +1012,7 @@ describe('ChatClient resume', () => { ]) }) - it('continues a legacy client tool emitted by a native resume', async () => { + it('continues a frontend tool call emitted by a native resume', async () => { const lookup = toolDefinition({ name: 'lookup', description: 'Look up', @@ -1346,18 +1038,23 @@ describe('ChatClient resume', () => { interrupts: [ { id: 'interrupt-1', - reason: 'approval_required', + reason: 'confirmation', metadata: { - kind: 'approval', - toolName: 'confirm', - input: {}, + 'tanstack:interruptBinding': { + v: INTERRUPT_BINDING_VERSION, + kind: 'generic', + interruptId: 'interrupt-1', + interruptedRunId: ctx?.runId ?? 'interrupted-run', + generation: 0, + responseSchemaHash: 'none', + }, }, }, ], }, }, ], - // The native resume emits a legacy client tool, not another interrupt. + // The native resume emits a frontend tool call, not another interrupt. (ctx) => [ { type: EventType.RUN_STARTED, @@ -1367,36 +1064,26 @@ describe('ChatClient resume', () => { }, { type: EventType.TOOL_CALL_START, - toolCallId: 'legacy-tool-call', + toolCallId: 'frontend-tool-call', toolCallName: 'lookup', toolName: 'lookup', timestamp: Date.now(), }, { type: EventType.TOOL_CALL_ARGS, - toolCallId: 'legacy-tool-call', + toolCallId: 'frontend-tool-call', delta: JSON.stringify({ query: 'answer' }), timestamp: Date.now(), }, - { - type: EventType.CUSTOM, - name: 'tool-input-available', - value: { - toolCallId: 'legacy-tool-call', - toolName: 'lookup', - input: { query: 'answer' }, - }, - timestamp: Date.now(), - }, { type: EventType.RUN_FINISHED, runId: ctx?.runId ?? 'resume-run', threadId: ctx?.threadId ?? 'thread-1', - metadata: { tanstack: { finishReason: 'tool_calls' } }, + metadata: { tanstack: { finishReason: 'stop' } }, timestamp: Date.now(), }, ], - // The legacy tool result continues through an ordinary request. + // The tool result continues through an ordinary request. (ctx) => [ { type: EventType.RUN_STARTED, diff --git a/packages/ai-client/tests/chat-client-stop-continuation.test.ts b/packages/ai-client/tests/chat-client-stop-continuation.test.ts deleted file mode 100644 index 446341261d..0000000000 --- a/packages/ai-client/tests/chat-client-stop-continuation.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { EventType } from '@tanstack/ai/client' -import { ChatClient } from '../src/chat-client' -import type { - ConnectConnectionAdapter, - RunAgentInputContext, -} from '../src/connection-adapters' - -describe('stop() vs queued continuation', () => { - it('does not auto-continue a legacy approval queued before stop()', async () => { - const contexts: Array = [] - let calls = 0 - let releaseFirstRun!: () => void - const firstRunHold = new Promise((resolve) => { - releaseFirstRun = resolve - }) - const connection: ConnectConnectionAdapter = { - async *connect(_messages, _data, abortSignal, context) { - contexts.push(context) - calls += 1 - const runId = context?.runId ?? `run-${calls}` - const threadId = context?.threadId ?? 'thread-1' - yield { - type: EventType.RUN_STARTED, - runId, - threadId, - timestamp: Date.now(), - } - if (calls === 1) { - yield { - type: EventType.TOOL_CALL_START, - toolCallId: 'tc-1', - toolCallName: 'dangerous_tool', - timestamp: Date.now(), - } - yield { - type: EventType.TOOL_CALL_ARGS, - toolCallId: 'tc-1', - delta: '{}', - timestamp: Date.now(), - } - yield { - type: EventType.TOOL_CALL_END, - toolCallId: 'tc-1', - timestamp: Date.now(), - } - yield { - type: EventType.CUSTOM, - name: 'approval-requested', - value: { - toolCallId: 'tc-1', - toolName: 'dangerous_tool', - input: {}, - approval: { id: 'approval-1', needsApproval: true }, - }, - timestamp: Date.now(), - } - await firstRunHold - if (abortSignal?.aborted) return - yield { - type: EventType.RUN_FINISHED, - runId, - threadId, - timestamp: Date.now(), - metadata: { tanstack: { finishReason: 'tool_calls' } }, - } - return - } - yield { - type: EventType.TEXT_MESSAGE_START, - messageId: `msg-${calls}`, - role: 'assistant', - timestamp: Date.now(), - } - yield { - type: EventType.TEXT_MESSAGE_CONTENT, - messageId: `msg-${calls}`, - delta: 'ok', - timestamp: Date.now(), - } - yield { - type: EventType.TEXT_MESSAGE_END, - messageId: `msg-${calls}`, - timestamp: Date.now(), - } - yield { - type: EventType.RUN_FINISHED, - runId, - threadId, - timestamp: Date.now(), - metadata: { tanstack: { finishReason: 'stop' } }, - } - }, - } - - const client = new ChatClient({ - connection, - threadId: 'thread-1', - }) - - const send = client.sendMessage('start') - await vi.waitFor(() => { - const hasApproval = client - .getMessages() - .some((message) => - message.parts.some( - (part) => - part.type === 'tool-call' && part.approval?.id === 'approval-1', - ), - ) - expect(hasApproval).toBe(true) - expect(client.getIsLoading()).toBe(true) - }) - await client.addToolApprovalResponse({ - id: 'approval-1', - approved: true, - }) - client.stop() - releaseFirstRun() - await send - await Promise.resolve() - await Promise.resolve() - - expect(calls).toBe(1) - expect(contexts).toHaveLength(1) - - await client.sendMessage('later') - expect(calls).toBe(2) - expect(contexts[1]?.parentRunId).toBeUndefined() - expect(contexts[1]?.resume).toBeUndefined() - }) -}) diff --git a/packages/ai-client/tests/chat-client.test.ts b/packages/ai-client/tests/chat-client.test.ts index 384b0eef25..0ebfb9ab60 100644 --- a/packages/ai-client/tests/chat-client.test.ts +++ b/packages/ai-client/tests/chat-client.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it, vi } from 'vitest' import { EventType, withTanstackMetadata } from '@tanstack/ai/client' import { ChatClient } from '../src/chat-client' import { - createApprovalToolCallChunks, createCustomEventChunks, createMockConnectionAdapter, createPushableSubscribeConnection, @@ -40,7 +39,9 @@ describe('ChatClient', () => { storedMessages?: Array | null, ): ChatClientPersistence { return { - getItem: vi.fn(() => storedMessages), + getItem: vi.fn(() => + storedMessages ? { messages: storedMessages } : storedMessages, + ), setItem: vi.fn(), removeItem: vi.fn(), } @@ -395,7 +396,7 @@ describe('ChatClient', () => { const adapter = createMockConnectionAdapter() const onMessagesChange = vi.fn() const persistence = { - getItem: vi.fn(() => Promise.resolve([persistedMessage])), + getItem: vi.fn(() => Promise.resolve({ messages: [persistedMessage] })), setItem: vi.fn(), removeItem: vi.fn(), } @@ -419,7 +420,7 @@ describe('ChatClient', () => { it('should ignore async persistence hydration after local message changes', async () => { const adapter = createMockConnectionAdapter() - const deferred = createDeferred>() + const deferred = createDeferred() const persistence = { getItem: vi.fn(() => deferred.promise), setItem: vi.fn(), @@ -442,7 +443,7 @@ describe('ChatClient', () => { }, ]) - deferred.resolve([persistedMessage]) + deferred.resolve({ messages: [persistedMessage] }) await new Promise((resolve) => setTimeout(resolve, 0)) expect(client.getMessages()).toEqual([ @@ -2650,7 +2651,7 @@ describe('ChatClient', () => { content: 'Notify me', }) - expect(client.getPendingInterrupts()).toEqual([ + expect(client.getInterrupts()).toEqual([ expect.objectContaining({ id: 'interrupt-1' }), ]) expect(client.getResumeState()?.runId).toBeTruthy() @@ -2705,7 +2706,7 @@ describe('ChatClient', () => { content: 'Notify me', }) - expect(client.getPendingInterrupts()).toEqual([ + expect(client.getInterrupts()).toEqual([ expect.objectContaining({ id: 'interrupt-direct' }), ]) expect(client.getResumeState()?.runId).toBeTruthy() @@ -3821,7 +3822,7 @@ describe('ChatClient', () => { ]) }) - it('should merge per-message body with base body', async () => { + it('should merge per-message body with forwardedProps', async () => { const chunks = createTextChunks('Response') let capturedData: Record | undefined const adapter = createMockConnectionAdapter({ @@ -3833,7 +3834,7 @@ describe('ChatClient', () => { const client = new ChatClient({ connection: adapter, - body: { model: 'gpt-5.5', temperature: 0.7 }, + forwardedProps: { model: 'gpt-5.5', temperature: 0.7 }, }) await client.sendMessage('Hello', { @@ -3859,7 +3860,7 @@ describe('ChatClient', () => { const client = new ChatClient({ connection: adapter, - body: { model: 'gpt-5.5', temperature: 0.7 }, + forwardedProps: { model: 'gpt-5.5', temperature: 0.7 }, }) // Framework hooks call sendMessage(content, undefined, sendOptions), @@ -3873,7 +3874,7 @@ describe('ChatClient', () => { expect(capturedData?.['maxTokens']).toBe(100) }) - it('merges chat body, positional body, and sendOptions.body with sendOptions.body winning conflicts', async () => { + it('merges forwardedProps, positional body, and sendOptions.body with sendOptions.body winning conflicts', async () => { const chunks = createTextChunks('Response') let capturedData: Record | undefined const adapter = createMockConnectionAdapter({ @@ -3885,7 +3886,7 @@ describe('ChatClient', () => { const client = new ChatClient({ connection: adapter, - body: { a: 1, b: 1, c: 1 }, + forwardedProps: { a: 1, b: 1, c: 1 }, }) await client.sendMessage( @@ -3933,7 +3934,7 @@ describe('ChatClient', () => { const client = new ChatClient({ connection: adapter, - body: { provider: 'openai' }, + forwardedProps: { provider: 'openai' }, }) await client.sendMessage('Hello', undefined, { @@ -3969,79 +3970,6 @@ describe('ChatClient', () => { expect(capturedData?.['model']).toBe('gpt-5.5') }) - it('updateOptions({ forwardedProps }) leaves a previously-set body intact', async () => { - const chunks = createTextChunks('Response') - const captures: Array | undefined> = [] - const adapter = createMockConnectionAdapter({ - chunks, - onConnect: (_messages, data) => { - captures.push(data) - }, - }) - - const client = new ChatClient({ - connection: adapter, - body: { provider: 'openai' }, - forwardedProps: { model: 'gpt-5.5' }, - }) - - // Replace only `forwardedProps` — `body` must survive. - client.updateOptions({ forwardedProps: { model: 'gpt-5.5' } }) - - await client.sendMessage('Hi') - expect(captures[0]?.['provider']).toBe('openai') - expect(captures[0]?.['model']).toBe('gpt-5.5') - }) - - it('updateOptions({ body }) leaves a previously-set forwardedProps intact', async () => { - const chunks = createTextChunks('Response') - const captures: Array | undefined> = [] - const adapter = createMockConnectionAdapter({ - chunks, - onConnect: (_messages, data) => { - captures.push(data) - }, - }) - - const client = new ChatClient({ - connection: adapter, - body: { provider: 'openai' }, - forwardedProps: { model: 'gpt-5.5' }, - }) - - client.updateOptions({ body: { provider: 'anthropic' } }) - - await client.sendMessage('Hi') - expect(captures[0]?.['provider']).toBe('anthropic') - expect(captures[0]?.['model']).toBe('gpt-5.5') - }) - - it('should merge body and forwardedProps with forwardedProps winning', async () => { - const chunks = createTextChunks('Response') - let capturedData: Record | undefined - const adapter = createMockConnectionAdapter({ - chunks, - onConnect: (_messages, data) => { - capturedData = data - }, - }) - - const client = new ChatClient({ - connection: adapter, - // Legacy `body` and new `forwardedProps` declared together — - // simulates a mid-migration codebase. - body: { model: 'gpt-5.5', temperature: 0.7 }, - forwardedProps: { model: 'gpt-5.5' }, - }) - - await client.sendMessage('Hello') - - // forwardedProps wins on key collision so partial migrations are sane. - expect(capturedData?.['model']).toBe('gpt-5.5') - // Non-conflicting keys from `body` are still forwarded. - expect(capturedData?.['temperature']).toBe(0.7) - }) - it('should not auto-emit `conversationId` in merged body (replaced by AG-UI threadId)', async () => { const chunks = createTextChunks('Response') let capturedData: Record | undefined @@ -4099,7 +4027,7 @@ describe('ChatClient', () => { const client = new ChatClient({ connection: adapter, - body: { model: 'gpt-5.5' }, + forwardedProps: { model: 'gpt-5.5' }, }) // First message with per-message body @@ -4227,21 +4155,6 @@ describe('ChatClient', () => { ) }) - it('should NOT call onCustomEvent for system events like tool-input-available', async () => { - const chunks = createToolCallChunks([ - { id: 'tc-1', name: 'testTool', arguments: '{}' }, - ]) - const adapter = createMockConnectionAdapter({ chunks }) - - const onCustomEvent = vi.fn() - const client = new ChatClient({ connection: adapter, onCustomEvent }) - - await client.sendMessage('Test tool call') - - // Should not have been called for tool-input-available system event - expect(onCustomEvent).not.toHaveBeenCalled() - }) - it('should work when onCustomEvent is not provided', async () => { const chunks = createCustomEventChunks([ { name: 'some-event', value: { info: 'test' } }, @@ -4342,126 +4255,6 @@ describe('ChatClient', () => { }) }) - describe('chained tool approvals', () => { - it('should continue after second approval arrives during active continuation stream', async () => { - let streamCount = 0 - let resolveStreamPause: (() => void) | null = null - - const adapter: ConnectionAdapter = { - async *connect() { - streamCount++ - - if (streamCount === 1) { - // First stream: tool call A needing approval - const chunks = createApprovalToolCallChunks([ - { - id: 'tc-1', - name: 'dangerous_tool_1', - arguments: '{}', - approvalId: 'approval-1', - }, - ]) - for (const chunk of chunks) yield chunk - } else if (streamCount === 2) { - // Second stream (after first approval): tool call B needing approval - // Yield the tool call and approval request - const preChunks: Array = [ - { - type: EventType.TOOL_CALL_START, - toolCallId: 'tc-2', - timestamp: Date.now(), - toolCallName: 'dangerous_tool_call_2', - }, - { - type: EventType.TOOL_CALL_ARGS, - toolCallId: 'tc-2', - timestamp: Date.now(), - delta: '{}', - }, - { - type: EventType.TOOL_CALL_END, - toolCallId: 'tc-2', - timestamp: Date.now(), - }, - { - type: EventType.CUSTOM, - timestamp: Date.now(), - name: 'approval-requested', - value: { - toolCallId: 'tc-2', - toolName: 'dangerous_tool_2', - input: {}, - approval: { id: 'approval-2', needsApproval: true }, - }, - }, - ] - for (const chunk of preChunks) yield chunk - - // Pause stream so the test can approve tool B while stream is active - await new Promise((resolve) => { - resolveStreamPause = resolve - }) - - yield { - type: EventType.RUN_FINISHED as const, - runId: 'run-2', - threadId: 'thread-1', - model: 'test', - timestamp: Date.now(), - metadata: { tanstack: { finishReason: 'tool_calls' } }, - } - } else if (streamCount === 3) { - // Third stream (after second approval): final text response - const chunks = createTextChunks('All done!') - for (const chunk of chunks) yield chunk - } - }, - } - - const client = new ChatClient({ connection: adapter }) - - // Step 1: Send message. First stream produces tool A with approval. - await client.sendMessage('Do something dangerous') - expect(streamCount).toBe(1) - - // Step 2: Approve tool A. This triggers checkForContinuation → streamResponse (stream 2). - // Don't await - we need to interact during the stream. - const approvalPromise = client.addToolApprovalResponse({ - id: 'approval-1', - approved: true, - }) - - // Wait for second stream to pause (approval-requested chunk already processed) - await vi.waitFor(() => { - expect(resolveStreamPause).not.toBeNull() - }) - - // Step 3: Approve tool B while second stream is still active (isLoading=true) - expect(client.getIsLoading()).toBe(true) - await client.addToolApprovalResponse({ - id: 'approval-2', - approved: true, - }) - - // Resume the second stream - resolveStreamPause!() - - // Wait for the full chain to complete - await approvalPromise - - // Step 4: Verify all 3 streams fired (the second approval triggered stream 3) - expect(streamCount).toBe(3) - - // Verify final text response is present - const messages = client.getMessages() - const lastAssistant = [...messages] - .reverse() - .find((m) => m.role === 'assistant') - const textPart = lastAssistant?.parts.find((p) => p.type === 'text') - expect(textPart?.content).toBe('All done!') - }) - }) - describe('concurrent runs and reconnect correctness', () => { it('concurrent runs should not produce duplicate messages or corrupt content', async () => { const { connection, push } = createPushableSubscribeConnection() diff --git a/packages/ai-client/tests/chat-fetcher.test.ts b/packages/ai-client/tests/chat-fetcher.test.ts index d45199d455..42a8dd569e 100644 --- a/packages/ai-client/tests/chat-fetcher.test.ts +++ b/packages/ai-client/tests/chat-fetcher.test.ts @@ -173,7 +173,7 @@ describe('ChatClient — fetcher transport', () => { const client = new ChatClient({ fetcher, - body: { provider: 'openai' }, + forwardedProps: { provider: 'openai' }, }) await client.sendMessage('hello there') diff --git a/packages/ai-client/tests/client-persistor.test.ts b/packages/ai-client/tests/client-persistor.test.ts index b6dba01723..cfc1b1fee5 100644 --- a/packages/ai-client/tests/client-persistor.test.ts +++ b/packages/ai-client/tests/client-persistor.test.ts @@ -85,7 +85,6 @@ describe('ChatPersistor', () => { const adapter = createMockPersistence(stored) const { persistor } = createPersistor(adapter) - // A legacy bare-array record is normalized to the combined shape. expect(persistor.readInitial()).toEqual({ messages: stored }) expect(adapter.getItem).toHaveBeenCalledWith(CHAT_ID) }) @@ -129,7 +128,7 @@ describe('ChatPersistor', () => { it('returns the promise from an asynchronous getItem', () => { const stored = [createUIMessage('m-1')] const adapter = createMockPersistence() - adapter.getItem = vi.fn(() => Promise.resolve(stored)) + adapter.getItem = vi.fn(() => Promise.resolve({ messages: stored })) const { persistor } = createPersistor(adapter) expect(persistor.readInitial()).toBeInstanceOf(Promise) diff --git a/packages/ai-client/tests/client-snapshots.test.ts b/packages/ai-client/tests/client-snapshots.test.ts index 3450436671..c1100e05dd 100644 --- a/packages/ai-client/tests/client-snapshots.test.ts +++ b/packages/ai-client/tests/client-snapshots.test.ts @@ -7,7 +7,7 @@ import { RealtimeClient, VideoGenerationClient, } from '../src' -import type { RealtimeAdapter, RealtimeConnection } from '../src' +import type { RealtimeAdapter, RealtimeConnection } from '@tanstack/ai/client' describe('client UI snapshots', () => { it('publishes and clears the active generation run id', async () => { diff --git a/packages/ai-client/tests/connection-adapters-cleanup.test.ts b/packages/ai-client/tests/connection-adapters-cleanup.test.ts index ed3e3d8b68..49776f3a55 100644 --- a/packages/ai-client/tests/connection-adapters-cleanup.test.ts +++ b/packages/ai-client/tests/connection-adapters-cleanup.test.ts @@ -85,21 +85,3 @@ describe.each([ expect(body.locked).toBe(false) }) }) - -it('cancels an SSE response after the DONE sentinel', async () => { - const cancel = vi.fn() - const body = new ReadableStream({ - start(controller) { - controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n')) - }, - cancel, - }) - const adapter = fetchServerSentEvents('/chat', { - fetchClient: async () => new Response(body), - }) - const chunks = [] - for await (const received of adapter.connect([])) chunks.push(received) - expect(chunks).toEqual([expect.objectContaining({ type: 'RUN_FINISHED' })]) - expect(cancel).toHaveBeenCalledOnce() - expect(body.locked).toBe(false) -}) diff --git a/packages/ai-client/tests/connection-adapters-xhr.test.ts b/packages/ai-client/tests/connection-adapters-xhr.test.ts index 4bc74ce24e..45ed8c90f9 100644 --- a/packages/ai-client/tests/connection-adapters-xhr.test.ts +++ b/packages/ai-client/tests/connection-adapters-xhr.test.ts @@ -110,7 +110,6 @@ describe('xhr connection adapters', () => { threadId: 'thread-1', runId: 'run-1', forwardedProps: { model: 'test-model', temperature: 0 }, - data: { model: 'test-model', temperature: 0 }, }) xhr.progress('data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg') @@ -166,70 +165,13 @@ describe('xhr connection adapters', () => { }, ]) - xhr.progress('data: [DONE]\n\n') - await expect(nextChunk).resolves.toMatchObject({ - done: false, - value: { type: EventType.RUN_FINISHED }, - }) - }) - - it('synthesizes RUN_FINISHED for [DONE] and ignores later bytes', async () => { - const { xhr, xhrFactory } = createFakeXhrFactory() - const adapter = xhrServerSentEvents('/api/chat', { xhrFactory }) - const iterator = adapter - .connect([], undefined, undefined, { - threadId: 'thread-1', - runId: 'run-1', - }) - [Symbol.asyncIterator]() - const nextChunk = iterator.next() - await nextTick() - - xhr.progress('data: [DONE]\n\n') xhr.progress( - 'data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"late","timestamp":1,"delta":"late","content":"late"}\n\n', + 'data: {"type":"RUN_FINISHED","threadId":"thread-1","runId":"run-1"}\n\n', ) - - await expect(nextChunk).resolves.toMatchObject({ - done: false, - value: { - type: EventType.RUN_FINISHED, - threadId: 'thread-1', - runId: 'run-1', - metadata: { tanstack: { finishReason: 'stop' } }, - }, - }) - await expect(iterator.next()).resolves.toEqual({ - done: true, - value: undefined, - }) - }) - - it('aborts the XHR after [DONE] so late bytes stop downloading', async () => { - const { xhr, xhrFactory } = createFakeXhrFactory() - const adapter = xhrServerSentEvents('/api/chat', { xhrFactory }) - const iterator = adapter - .connect([], undefined, undefined, { - threadId: 'thread-1', - runId: 'run-1', - }) - [Symbol.asyncIterator]() - const nextChunk = iterator.next() - await nextTick() - - xhr.progress('data: [DONE]\n\n') - await expect(nextChunk).resolves.toMatchObject({ done: false, value: { type: EventType.RUN_FINISHED }, }) - // Completing iteration tears the socket down so late bytes stop - // downloading (the terminal event's yield pauses before cleanup). - await expect(iterator.next()).resolves.toEqual({ - done: true, - value: undefined, - }) - expect(xhr.abort).toHaveBeenCalledTimes(1) }) it('parses SSE data frames without a space after the colon', async () => { @@ -363,8 +305,11 @@ describe('xhr connection adapters', () => { [Symbol.asyncIterator]() const successNext = successIterator.next() await nextTick() - success.xhr.progress('data: [DONE]\n\n') + success.xhr.progress( + 'data: {"type":"RUN_FINISHED","threadId":"thread-1","runId":"run-1"}\n\n', + ) await successNext + success.xhr.load() await successIterator.next() expect(successRemoveEventListener).toHaveBeenCalledWith( diff --git a/packages/ai-client/tests/connection-adapters.test.ts b/packages/ai-client/tests/connection-adapters.test.ts index 3f6c6ed169..307266bb80 100644 --- a/packages/ai-client/tests/connection-adapters.test.ts +++ b/packages/ai-client/tests/connection-adapters.test.ts @@ -297,40 +297,6 @@ describe('connection-adapters', () => { expect(chunks).toHaveLength(1) }) - it('should synthesize RUN_FINISHED on [DONE] and stop reading', async () => { - const mockReader = { - read: vi - .fn() - .mockResolvedValueOnce({ - done: false, - value: new TextEncoder().encode('data: [DONE]\n\n'), - }) - .mockResolvedValueOnce({ done: true, value: undefined }), - releaseLock: vi.fn(), - } - - const mockResponse = { - ok: true, - body: { - getReader: () => mockReader, - }, - } - - fetchMock.mockResolvedValue(mockResponse as any) - - const adapter = fetchServerSentEvents('/api/chat') - const chunks: Array = [] - - for await (const chunk of adapter.connect([ - { role: 'user', content: 'Hello' }, - ])) { - chunks.push(chunk) - } - - expect(chunks).toHaveLength(1) - expect(chunks[0]!.type).toBe('RUN_FINISHED') - }) - it('should throw a SyntaxError on malformed JSON', async () => { const mockReader = { read: vi @@ -569,35 +535,6 @@ describe('connection-adapters', () => { expect(body.forwardedProps).toMatchObject({ key: 'value' }) }) - it('should mirror forwardedProps under legacy `data` field for backward-compat', async () => { - const mockReader = { - read: vi.fn().mockResolvedValue({ done: true, value: undefined }), - releaseLock: vi.fn(), - } - const mockResponse = { - ok: true, - body: { getReader: () => mockReader }, - } - fetchMock.mockResolvedValue(mockResponse as any) - - const adapter = fetchServerSentEvents('/api/chat') - - for await (const _ of adapter.connect( - [{ role: 'user', content: 'Hello' }], - { provider: 'openai', model: 'gpt-4o' }, - )) { - // Consume - } - - const call = fetchMock.mock.calls[0] - const body = JSON.parse(call?.[1]?.body as string) - // Legacy server code reads `body.data.X`; new server code reads - // `body.forwardedProps.X`. Both must contain the same content - // until the legacy `body` client option is removed. - expect(body.data).toEqual(body.forwardedProps) - expect(body.data).toMatchObject({ provider: 'openai', model: 'gpt-4o' }) - }) - it('should use custom fetchClient when provided', async () => { const customFetch = vi.fn() const mockReader = { diff --git a/packages/ai-client/tests/devtools.test.ts b/packages/ai-client/tests/devtools.test.ts index f7cb2b034c..5f67967a06 100644 --- a/packages/ai-client/tests/devtools.test.ts +++ b/packages/ai-client/tests/devtools.test.ts @@ -184,15 +184,6 @@ describe('ChatClient devtools bridge', () => { return latestSnapshot?.state?.messages ?? [] } - function findToolCallPart(messages: Array, toolCallId: string) { - return messages - .flatMap((message) => message.parts) - .find( - (part): part is Extract => - part.type === 'tool-call' && part.id === toolCallId, - ) - } - function findStructuredOutputPart( messages: Array, messageId: string, @@ -1279,8 +1270,6 @@ describe('ChatClient devtools bridge', () => { ...createToolCallChunks( [{ id: 'call-1', name: 'weather', arguments: '{"city":"Paris"}' }], 'msg-tool', - 'test', - false, ), ], ], @@ -1958,138 +1947,6 @@ describe('ChatClient devtools bridge', () => { client.dispose() }) - it('emits approval requests that arrive after run finish', async () => { - const runContexts: Array = [] - const chunks: Array = [ - ...createToolCallChunks( - [ - { - id: 'approval-call-1', - name: 'addToCart', - arguments: '{"guitarId":"6","quantity":1}', - }, - ], - 'msg-approval', - 'test', - false, - ), - { - type: EventType.CUSTOM, - metadata: { tanstack: { model: 'test' } }, - timestamp: Date.now(), - name: 'approval-requested', - value: { - toolCallId: 'approval-call-1', - toolName: 'addToCart', - input: { guitarId: '6', quantity: 1 }, - approval: { id: 'approval-approval-call-1', needsApproval: true }, - }, - }, - ] - const client = createClient({ - connection: createRunTrackingAdapter([chunks], runContexts), - }) - vi.clearAllMocks() - - await client.sendMessage('add it to cart') - - await waitForCondition( - () => eventClientMock.emitted('tools:approval:requested').length > 0, - ) - expect(eventClientMock.emitted('tools:approval:requested')).toEqual([ - [ - 'tools:approval:requested', - expect.objectContaining({ - hookId: 'thread-1', - clientId: 'thread-1', - threadId: runContexts[0]?.threadId, - runId: runContexts[0]?.runId, - streamId: expect.any(String), - messageId: expect.any(String), - toolCallId: 'approval-call-1', - toolName: 'addToCart', - input: { guitarId: '6', quantity: 1 }, - approvalId: 'approval-approval-call-1', - }), - ], - ]) - - await waitForCondition(() => { - const toolCall = findToolCallPart( - latestSnapshotMessages(), - 'approval-call-1', - ) - return ( - toolCall?.state === 'approval-requested' && - toolCall.approval?.id === 'approval-approval-call-1' - ) - }) - - client.dispose() - }) - - it('emits approval responses and snapshots the approval decision', async () => { - const runContexts: Array = [] - const client = createClient({ - connection: createRunTrackingAdapter( - [createTextChunks('approved', 'msg-after-approval')], - runContexts, - ), - initialMessages: [ - userMessage, - { - id: 'msg-approval', - role: 'assistant', - parts: [ - { - type: 'tool-call', - id: 'approval-call-1', - name: 'addToCart', - arguments: '{"guitarId":"6","quantity":1}', - input: { guitarId: '6', quantity: 1 }, - state: 'approval-requested', - approval: { - id: 'approval-approval-call-1', - needsApproval: true, - }, - }, - ], - }, - ], - }) - vi.clearAllMocks() - - await client.addToolApprovalResponse({ - id: 'approval-approval-call-1', - approved: true, - }) - - expect(eventClientMock.emitted('tools:approval:responded')).toEqual([ - [ - 'tools:approval:responded', - expect.objectContaining({ - hookId: 'thread-1', - clientId: 'thread-1', - toolCallId: 'approval-call-1', - approvalId: 'approval-approval-call-1', - approved: true, - }), - ], - ]) - await waitForCondition(() => { - const toolCall = findToolCallPart( - latestSnapshotMessages(), - 'approval-call-1', - ) - return ( - toolCall?.state === 'approval-responded' && - toolCall.approval?.approved === true - ) - }) - - client.dispose() - }) - it('keeps delayed client tool results linked to their original run context', async () => { let resolveTool!: (output: unknown) => void let markToolStarted!: () => void diff --git a/packages/ai-client/tests/events.test.ts b/packages/ai-client/tests/events.test.ts index be53de075a..cdc0300ef1 100644 --- a/packages/ai-client/tests/events.test.ts +++ b/packages/ai-client/tests/events.test.ts @@ -296,25 +296,6 @@ describe('events', () => { }) }) - it('emits tools:approval:responded with user-visible envelope and run context', () => { - emitter.toolApprovalResponded('approval-1', 'call-1', true, { - threadId: 'thread-1', - runId: 'run-1', - }) - - expect(aiEventClient.emit).toHaveBeenCalledWith( - 'tools:approval:responded', - { - approvalId: 'approval-1', - toolCallId: 'call-1', - approved: true, - threadId: 'thread-1', - runId: 'run-1', - ...expectedEnvelope('tools:approval:responded', 'user-visible'), - }, - ) - }) - it('emits devtools:tool-fixture:applied with a user-visible envelope', () => { emitter.toolFixtureApplied({ hookId: 'test-client-id', diff --git a/packages/ai-client/tests/interrupts-types.test-d.ts b/packages/ai-client/tests/interrupts-types.test-d.ts index 4a4e191047..91a85b0a2d 100644 --- a/packages/ai-client/tests/interrupts-types.test-d.ts +++ b/packages/ai-client/tests/interrupts-types.test-d.ts @@ -160,8 +160,7 @@ expectTypeOf>().toEqualTypeOf< declare const client: ChatClient client.updateOptions({ - onInterruptStateChange: (state, context) => { - expectTypeOf(state.interrupts).toEqualTypeOf(state.pendingInterrupts) + onInterruptStateChange: (_state, context) => { expectTypeOf(context.source).toEqualTypeOf<'hydrate' | 'live'>() }, }) diff --git a/packages/ai-client/tests/resume-snapshot.test.ts b/packages/ai-client/tests/resume-snapshot.test.ts index 1217f400a9..63818a5995 100644 --- a/packages/ai-client/tests/resume-snapshot.test.ts +++ b/packages/ai-client/tests/resume-snapshot.test.ts @@ -24,9 +24,9 @@ import type { import type { StreamChunk } from '@tanstack/ai/client' /** An in-memory store capturing the last combined record written. */ -function memoryAdapter(initial?: ChatPersistedState | Array): { +function memoryAdapter(initial?: ChatPersistedState): { adapter: ChatClientPersistence - read: () => ChatPersistedState | Array | undefined + read: () => ChatPersistedState | undefined } { let value = initial return { @@ -89,17 +89,6 @@ describe('ChatPersistor combined record', () => { expect(stored.messages).toHaveLength(1) expect(stored.resume).toBeUndefined() }) - - it('normalizes a legacy bare-array record on read', () => { - const applied: Array> = [] - const { adapter } = memoryAdapter([createUIMessage('m1', 'legacy')]) - const persistor = new ChatPersistor(adapter, 'chat-1', (m) => - applied.push(m), - ) - const state = persistor.readInitial() as ChatPersistedState - expect(state.messages[0]?.id).toBe('m1') - expect(state.resume).toBeUndefined() - }) }) describe('localStoragePersistence ergonomics', () => { @@ -847,7 +836,7 @@ describe('ChatClient auto-rejoin after reload', () => { ) const stored = read() - if (!stored || Array.isArray(stored)) { + if (!stored) { throw new Error('expected a persisted record with a resume pointer') } expect(stored.resume?.resumeState?.runId).toBe('quiet-run') @@ -894,14 +883,7 @@ describe('ChatClient auto-rejoin after reload', () => { // Dead pointer must be removed so the next load does not re-pin loading. await vi.waitFor(() => { - const stored = read() - if (stored && !Array.isArray(stored)) { - expect(stored.resume).toBeUndefined() - } else { - // A bare message array (or a removed key) carries no resume pointer by - // construction — nothing further to assert on the record shape. - expect(stored === undefined || Array.isArray(stored)).toBe(true) - } + expect(read()?.resume).toBeUndefined() }) void client }) diff --git a/packages/ai-client/tests/sse-done-model.test.ts b/packages/ai-client/tests/sse-done-model.test.ts deleted file mode 100644 index 86c45c884e..0000000000 --- a/packages/ai-client/tests/sse-done-model.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { EventType } from '@tanstack/ai/client' -import { fetchServerSentEvents } from '../src/connection-adapters' -import type { StreamChunk } from '@tanstack/ai/client' - -function sseResponse(body: string) { - const encoder = new TextEncoder() - return { - ok: true, - body: { - getReader: () => ({ - read: vi - .fn() - .mockResolvedValueOnce({ - done: false, - value: encoder.encode(body), - }) - .mockResolvedValueOnce({ done: true, value: undefined }), - releaseLock: vi.fn(), - }), - }, - } -} - -async function collectSse(body: string): Promise> { - const fetchClient = vi.fn().mockResolvedValue(sseResponse(body)) - const adapter = fetchServerSentEvents('/api/chat', { fetchClient }) - const chunks: Array = [] - for await (const chunk of adapter.connect([ - { role: 'user', content: 'Hello' }, - ])) { - chunks.push(chunk) - } - return chunks -} - -describe('SSE [DONE] synthetic RUN_FINISHED', () => { - it('copies metadata.tanstack.model from RUN_STARTED and sets finishReason stop', async () => { - const started = JSON.stringify({ - type: EventType.RUN_STARTED, - threadId: 'thread-1', - runId: 'run-1', - timestamp: 1, - metadata: { tanstack: { model: 'gpt-5.5' } }, - }) - const chunks = await collectSse(`data: ${started}\n\ndata: [DONE]\n\n`) - - expect(chunks).toHaveLength(2) - expect(chunks[0]!.type).toBe(EventType.RUN_STARTED) - const done = chunks[1] - if (done === undefined || done.type !== EventType.RUN_FINISHED) { - throw new Error('expected RUN_FINISHED') - } - expect(done).not.toHaveProperty('model') - expect(done).not.toHaveProperty('finishReason') - expect(done.metadata?.tanstack).toEqual({ - model: 'gpt-5.5', - finishReason: 'stop', - }) - }) -}) diff --git a/packages/ai-client/tests/test-utils.ts b/packages/ai-client/tests/test-utils.ts index dec8accbdf..889923472e 100644 --- a/packages/ai-client/tests/test-utils.ts +++ b/packages/ai-client/tests/test-utils.ts @@ -43,7 +43,7 @@ export function createMockPersistence( initial?: Array | null, ): ChatClientPersistence { return { - getItem: vi.fn(() => initial), + getItem: vi.fn(() => (initial ? { messages: initial } : initial)), setItem: vi.fn(), removeItem: vi.fn(), } @@ -280,14 +280,13 @@ export function createCustomEventChunks( } /** - * Helper to create tool call chunks (AG-UI format) - * Optionally includes tool-input-available chunks to trigger onToolCall + * Helper to create tool call chunks (AG-UI format). The RUN_FINISHED leaves + * the calls unanswered, so the client runs them (onToolCall). */ export function createToolCallChunks( toolCalls: Array<{ id: string; name: string; arguments: string }>, messageId: string = 'msg-1', model: string = 'test', - includeToolInputAvailable: boolean = true, ): Array { const chunks: Array = [] const runId = `run-${messageId}` @@ -308,99 +307,13 @@ export function createToolCallChunks( timestamp: Date.now(), delta: toolCall.arguments, } as StreamChunk) - - // Add tool-input-available CUSTOM chunk if requested - if (includeToolInputAvailable) { - let parsedInput: any - try { - parsedInput = JSON.parse(toolCall.arguments) - } catch { - parsedInput = toolCall.arguments - } - - chunks.push({ - type: 'CUSTOM', - timestamp: Date.now(), - name: 'tool-input-available', - value: { - toolCallId: toolCall.id, - toolName: toolCall.name, - input: parsedInput, - }, - } as StreamChunk) - } } chunks.push( runFinishedChunk({ runId, threadId: `thread-${messageId}`, - finishReason: 'tool_calls', - model, - }), - ) - - return chunks -} - -/** - * Helper to create tool call chunks with approval requests (AG-UI format) - * Tools will be in 'input-complete' state with pending approval - */ -export function createApprovalToolCallChunks( - toolCalls: Array<{ - id: string - name: string - arguments: string - approvalId: string - }>, - messageId: string = 'msg-1', - model: string = 'test', -): Array { - const chunks: Array = [] - const runId = `run-${messageId}` - - for (let i = 0; i < toolCalls.length; i++) { - const toolCall = toolCalls[i]! - - chunks.push({ - type: 'TOOL_CALL_START', - toolCallId: toolCall.id, - toolCallName: toolCall.name, - timestamp: Date.now(), - } as StreamChunk) - - chunks.push({ - type: 'TOOL_CALL_ARGS', - toolCallId: toolCall.id, - timestamp: Date.now(), - delta: toolCall.arguments, - } as StreamChunk) - - chunks.push({ - type: 'TOOL_CALL_END', - toolCallId: toolCall.id, - timestamp: Date.now(), - } as StreamChunk) - - chunks.push({ - type: 'CUSTOM', - timestamp: Date.now(), - name: 'approval-requested', - value: { - toolCallId: toolCall.id, - toolName: toolCall.name, - input: JSON.parse(toolCall.arguments), - approval: { id: toolCall.approvalId, needsApproval: true }, - }, - } as StreamChunk) - } - - chunks.push( - runFinishedChunk({ - runId, - threadId: `thread-${messageId}`, - finishReason: 'tool_calls', + finishReason: 'stop', model, }), ) diff --git a/packages/ai-client/tests/ui-fixtures.ts b/packages/ai-client/tests/ui-fixtures.ts index 994f06b5f6..6e9c0807e8 100644 --- a/packages/ai-client/tests/ui-fixtures.ts +++ b/packages/ai-client/tests/ui-fixtures.ts @@ -242,7 +242,6 @@ export function createChatResult(init: { messages: init.messages ?? [], subagents: init.subagents ?? [], interrupts, - pendingInterrupts: interrupts, error: init.error, isLoading: init.isLoading ?? false, status: init.status ?? 'ready', @@ -269,7 +268,6 @@ export function createSolidChatResult( ...chat, messages: () => chat.messages, interrupts: () => chat.interrupts, - pendingInterrupts: () => chat.pendingInterrupts, error: () => chat.error, isLoading: () => chat.isLoading, status: () => chat.status, @@ -290,7 +288,6 @@ export function createVueChatResult( ...chat, messages: { value: chat.messages }, interrupts: { value: chat.interrupts }, - pendingInterrupts: { value: chat.pendingInterrupts }, error: { value: chat.error }, isLoading: { value: chat.isLoading }, status: { value: chat.status }, diff --git a/packages/ai-cohere/src/adapters/rerank.ts b/packages/ai-cohere/src/adapters/rerank.ts index 075fc56cdd..55b2619768 100644 --- a/packages/ai-cohere/src/adapters/rerank.ts +++ b/packages/ai-cohere/src/adapters/rerank.ts @@ -120,10 +120,7 @@ export class CohereRerankAdapter< completionTokens: 0, totalTokens: 0, ...(searchUnits !== undefined - ? { - billed: { quantity: searchUnits, unit: 'units' }, - unitsBilled: searchUnits, - } + ? { billed: { quantity: searchUnits, unit: 'units' } } : {}), } diff --git a/packages/ai-cohere/tests/rerank-adapter.test.ts b/packages/ai-cohere/tests/rerank-adapter.test.ts index 4300de0694..3b502d55d0 100644 --- a/packages/ai-cohere/tests/rerank-adapter.test.ts +++ b/packages/ai-cohere/tests/rerank-adapter.test.ts @@ -101,7 +101,6 @@ describe('CohereRerankAdapter', () => { { index: 0, score: 0.12, document: documents[0] }, ]) expect(result.usage.billed).toEqual({ quantity: 1, unit: 'units' }) - expect(result.usage.unitsBilled).toBe(1) expect(result.usage.totalTokens).toBe(0) }) diff --git a/packages/ai-devtools/src/store/ai-context.tsx b/packages/ai-devtools/src/store/ai-context.tsx index 9ba45553fd..f4a005804c 100644 --- a/packages/ai-devtools/src/store/ai-context.tsx +++ b/packages/ai-devtools/src/store/ai-context.tsx @@ -1737,65 +1737,6 @@ export const AIProvider: ParentComponent = (props) => { }), ) - cleanupFns.push( - aiEventClient.on('tools:approval:responded', (e) => { - const { - clientId, - threadId, - streamId, - toolCallId, - approvalId, - approved, - timestamp, - } = e.payload - - const conversationIds = getToolEventConversationIds({ - clientId, - threadId, - streamId, - toolCallId, - approvalId, - }) - - for (const conversationId of conversationIds) { - const conv = state.conversations[conversationId] - if (!conv) continue - - const location = findToolCallLocation(conversationId, { - toolCallId, - approvalId, - }) - if (!location) continue - - updateToolCall( - conversationId, - location.messageIndex, - location.toolCallIndex, - { - state: approved ? 'approved' : 'denied', - approvalRequired: false, - approvalId, - approvalApproved: approved, - }, - ) - - const message = conv.messages[location.messageIndex] - const chunk: Chunk = { - id: `chunk-${Date.now()}-${Math.random()}`, - type: 'approval', - ...(message?.id ? { messageId: message.id } : {}), - toolCallId, - approvalId, - approved, - timestamp, - chunkCount: 1, - } - - addChunkToMessage(conversationId, chunk) - } - }), - ) - cleanupFns.push( aiEventClient.on('tools:call:updated', (e) => { const { diff --git a/packages/ai-event-client/src/devtools-middleware.ts b/packages/ai-event-client/src/devtools-middleware.ts index ec626af77a..9da264672f 100644 --- a/packages/ai-event-client/src/devtools-middleware.ts +++ b/packages/ai-event-client/src/devtools-middleware.ts @@ -171,7 +171,6 @@ interface DevtoolsMiddlewareContext { streamId: string runId: string threadId: string - conversationId?: string provider: string model: string source: 'client' | 'server' @@ -267,7 +266,7 @@ function buildEventContext(ctx: DevtoolsMiddlewareContext) { threadId: ctx.threadId, provider: ctx.provider, model: ctx.model, - clientId: ctx.conversationId, + clientId: ctx.threadId, source: ctx.source, // Devtools wire payload is plain strings; per-prompt metadata is // irrelevant for observation and would require devtools-UI changes to @@ -331,14 +330,12 @@ export function devtoolsMiddleware(): DevtoolsChatMiddleware { timestamp: Date.now(), }) - // Emit text:message:created for initial messages + // Emit text:message:created for the new user message const messages = ctx.messages - const messagesToEmit = ctx.conversationId - ? messages.slice(-1).filter((m) => m.role === 'user') - : messages + const messagesToEmit = messages.slice(-1).filter((m) => m.role === 'user') - messagesToEmit.forEach((message, index) => { - const messageIndex = ctx.conversationId ? messages.length - 1 : index + messagesToEmit.forEach((message) => { + const messageIndex = messages.length - 1 const messageId = ctx.createId('msg') const base = buildEventContext(ctx) const content = getContentString(message.content) diff --git a/packages/ai-event-client/src/index.ts b/packages/ai-event-client/src/index.ts index 9d0343f638..376e3156dc 100644 --- a/packages/ai-event-client/src/index.ts +++ b/packages/ai-event-client/src/index.ts @@ -284,19 +284,6 @@ export interface TokenUsage { * unit. A quantity, distinct from the monetary `cost` / `costDetails`. */ billed?: BilledUsage - /** - * @deprecated Read {@link TokenUsage.billed} instead, which pairs the same - * duration with an explicit `unit: 'seconds'`. Still populated alongside - * `billed` for backward compatibility; will be removed in a future release. - */ - durationSeconds?: number - /** - * @deprecated Read {@link TokenUsage.billed} instead, which pairs the same - * count with the unit it is denominated in (`seconds`, `units`, …) — this - * bare count is ambiguous across providers. Still populated alongside - * `billed` for backward compatibility; will be removed in a future release. - */ - unitsBilled?: number /** Provider-specific usage details not covered by standard fields */ providerUsageDetails?: TProviderDetails /** Provider-reported cost for the request, when available. */ @@ -327,13 +314,6 @@ export type ToolResultState = | 'complete' // Result is complete | 'error' // Error occurred -/** - * @deprecated Image and audio usage now use the canonical {@link TokenUsage} - * shape. Kept as an alias for backward compatibility; will be removed in a - * future release. - */ -export type ImageUsage = TokenUsage - // All optional fields explicitly allow `| undefined` so that callers // can spread shared-context builders (which set every field to a // possibly-undefined value) without `exactOptionalPropertyTypes` @@ -594,13 +574,6 @@ export interface ToolsApprovalRequestedEvent extends BaseEventContext { approvalId: string } -/** Emitted when user responds to an approval request. */ -export interface ToolsApprovalRespondedEvent extends BaseEventContext { - toolCallId: string - approvalId: string - approved: boolean -} - /** Emitted when tool input is available for client execution. */ export interface ToolsInputAvailableEvent extends BaseEventContext { requestId?: string @@ -1235,9 +1208,6 @@ export interface CompactionEndedEvent extends BaseEventContext { durationMs?: number } -/** @deprecated Use {@link CompactionStateEvent}. */ -export type CompactionAppliedEvent = CompactionStateEvent - // --------------------------------------------------------------------------- // Memory events // --------------------------------------------------------------------------- @@ -1526,7 +1496,6 @@ export interface AIDevtoolsEventMap { // Tool events 'tools:approval:requested': ToolsApprovalRequestedEvent - 'tools:approval:responded': ToolsApprovalRespondedEvent 'tools:input:available': ToolsInputAvailableEvent 'tools:call:completed': ToolsCallCompletedEvent 'tools:result:added': ToolsResultAddedEvent @@ -1612,7 +1581,6 @@ export interface AIDevtoolsEventMap { 'compaction:started': CompactionStartedEvent 'compaction:state': CompactionStateEvent 'compaction:ended': CompactionEndedEvent - 'compaction:applied': CompactionAppliedEvent // Memory events 'memory:retrieve:started': MemoryRetrieveStartedEvent diff --git a/packages/ai-fal/src/utils/billing.ts b/packages/ai-fal/src/utils/billing.ts index 4b5335d01f..8a9d293200 100644 --- a/packages/ai-fal/src/utils/billing.ts +++ b/packages/ai-fal/src/utils/billing.ts @@ -88,7 +88,6 @@ export function buildFalUsage( completionTokens: 0, totalTokens: 0, billed: { quantity: unitsBilled, unit: 'units' }, - unitsBilled, } } diff --git a/packages/ai-fal/tests/audio-adapter.test.ts b/packages/ai-fal/tests/audio-adapter.test.ts index b19a770d1e..36e23a8359 100644 --- a/packages/ai-fal/tests/audio-adapter.test.ts +++ b/packages/ai-fal/tests/audio-adapter.test.ts @@ -367,7 +367,6 @@ describe('Fal Audio Adapter', () => { completionTokens: 0, totalTokens: 0, billed: { quantity: 2.5, unit: 'units' }, - unitsBilled: 2.5, }) }) diff --git a/packages/ai-fal/tests/billing.test.ts b/packages/ai-fal/tests/billing.test.ts index 7b922393a7..1e0179a1d0 100644 --- a/packages/ai-fal/tests/billing.test.ts +++ b/packages/ai-fal/tests/billing.test.ts @@ -86,7 +86,6 @@ describe('buildFalUsage', () => { completionTokens: 0, totalTokens: 0, billed: { quantity: 4, unit: 'units' }, - unitsBilled: 4, }) }) @@ -96,7 +95,6 @@ describe('buildFalUsage', () => { completionTokens: 0, totalTokens: 0, billed: { quantity: 0, unit: 'units' }, - unitsBilled: 0, }) }) }) diff --git a/packages/ai-fal/tests/image-adapter.test.ts b/packages/ai-fal/tests/image-adapter.test.ts index d774685adc..4e93e67d28 100644 --- a/packages/ai-fal/tests/image-adapter.test.ts +++ b/packages/ai-fal/tests/image-adapter.test.ts @@ -380,7 +380,6 @@ describe('Fal Image Adapter', () => { completionTokens: 0, totalTokens: 0, billed: { quantity: 4, unit: 'units' }, - unitsBilled: 4, }) }) diff --git a/packages/ai-fal/tests/speech-adapter.test.ts b/packages/ai-fal/tests/speech-adapter.test.ts index 090c7e95dd..e61c78b7aa 100644 --- a/packages/ai-fal/tests/speech-adapter.test.ts +++ b/packages/ai-fal/tests/speech-adapter.test.ts @@ -339,7 +339,6 @@ describe('Fal Speech Adapter', () => { completionTokens: 0, totalTokens: 0, billed: { quantity: 3, unit: 'units' }, - unitsBilled: 3, }) }) diff --git a/packages/ai-fal/tests/transcription-adapter.test.ts b/packages/ai-fal/tests/transcription-adapter.test.ts index 3e72b6485c..0226955329 100644 --- a/packages/ai-fal/tests/transcription-adapter.test.ts +++ b/packages/ai-fal/tests/transcription-adapter.test.ts @@ -322,7 +322,6 @@ describe('Fal Transcription Adapter', () => { completionTokens: 0, totalTokens: 0, billed: { quantity: 1.5, unit: 'units' }, - unitsBilled: 1.5, }) }) diff --git a/packages/ai-fal/tests/video-adapter.test.ts b/packages/ai-fal/tests/video-adapter.test.ts index 035902dcec..55141660fb 100644 --- a/packages/ai-fal/tests/video-adapter.test.ts +++ b/packages/ai-fal/tests/video-adapter.test.ts @@ -536,7 +536,6 @@ describe('Fal Video Adapter', () => { completionTokens: 0, totalTokens: 0, billed: { quantity: 12, unit: 'units' }, - unitsBilled: 12, }) }) diff --git a/packages/ai-gemini/src/adapters/image.ts b/packages/ai-gemini/src/adapters/image.ts index 74f9729d62..3715b4b53e 100644 --- a/packages/ai-gemini/src/adapters/image.ts +++ b/packages/ai-gemini/src/adapters/image.ts @@ -141,7 +141,7 @@ export class GeminiImageAdapter< if (resolved.images.length > 0) { throw new Error( `${this.name}: model "${model}" (Imagen) does not support image prompt parts. ` + - `Use a Gemini-native image model (e.g. gemini-2.5-flash-image, "nano-banana") for image-conditioned generation.`, + `Use a Gemini-native image model (e.g. gemini-3.1-flash-image) for image-conditioned generation.`, ) } @@ -475,18 +475,6 @@ export class GeminiImageAdapter< } } -/** @deprecated Shut down 2026-06-25. Use `gemini-3.1-flash-image`. */ -export function createGeminiImage( - model: 'gemini-3.1-flash-image-preview', - apiKey: string, - config?: Omit, -): GeminiImageAdapter<'gemini-3.1-flash-image-preview'> -/** @deprecated Shut down 2026-06-25. Use `gemini-3-pro-image`. */ -export function createGeminiImage( - model: 'gemini-3-pro-image-preview', - apiKey: string, - config?: Omit, -): GeminiImageAdapter<'gemini-3-pro-image-preview'> /** * Creates a Gemini image adapter with explicit API key. * Type resolution happens here at the call site. @@ -506,11 +494,6 @@ export function createGeminiImage( * }); * ``` */ -export function createGeminiImage( - model: TModel, - apiKey: string, - config?: Omit, -): GeminiImageAdapter export function createGeminiImage( model: TModel, apiKey: string, @@ -519,16 +502,6 @@ export function createGeminiImage( return new GeminiImageAdapter({ apiKey, ...config }, model) } -/** @deprecated Shut down 2026-06-25. Use `gemini-3.1-flash-image`. */ -export function geminiImage( - model: 'gemini-3.1-flash-image-preview', - config?: Omit, -): GeminiImageAdapter<'gemini-3.1-flash-image-preview'> -/** @deprecated Shut down 2026-06-25. Use `gemini-3-pro-image`. */ -export function geminiImage( - model: 'gemini-3-pro-image-preview', - config?: Omit, -): GeminiImageAdapter<'gemini-3-pro-image-preview'> /** * Creates a Gemini image adapter with automatic API key detection from environment variables. * Type resolution happens here at the call site. @@ -553,10 +526,6 @@ export function geminiImage( * }); * ``` */ -export function geminiImage( - model: TModel, - config?: Omit, -): GeminiImageAdapter export function geminiImage( model: TModel, config?: Omit, diff --git a/packages/ai-gemini/src/adapters/text.ts b/packages/ai-gemini/src/adapters/text.ts index 16499b1845..3a008fb9cc 100644 --- a/packages/ai-gemini/src/adapters/text.ts +++ b/packages/ai-gemini/src/adapters/text.ts @@ -850,7 +850,6 @@ export class GeminiTextAdapter< let responseId: string | undefined let reportedModel: string | undefined let accumulatedContent = '' - let accumulatedThinking = '' const toolCallMap = new Map< string, { @@ -867,12 +866,10 @@ export class GeminiTextAdapter< const runId = options.runId ?? generateId(this.name) const threadId = options.threadId ?? generateId(this.name) const messageId = generateId(this.name) - let stepId: string | null = null let reasoningMessageId: string | null = null let hasClosedReasoning = false let hasEmittedRunStarted = false let hasEmittedTextMessageStart = false - let hasEmittedStepStarted = false let groundingMetadata: GroundingMetadata | undefined let groundingCallEmitted = false const adapterName = this.name @@ -952,10 +949,8 @@ export class GeminiTextAdapter< for (const part of parts) { if (part.text) { if (part.thought) { - // Emit STEP_STARTED and REASONING events on first thinking content - if (!hasEmittedStepStarted) { - hasEmittedStepStarted = true - stepId = generateId(this.name) + // Emit REASONING start events on first thinking content + if (!reasoningMessageId) { reasoningMessageId = generateId(this.name) // Spec REASONING events @@ -972,23 +967,9 @@ export class GeminiTextAdapter< model, timestamp: Date.now(), } - - // Legacy STEP events (kept during transition) - yield { - type: EventType.STEP_STARTED, - stepName: stepId, - stepId, - model, - timestamp: Date.now(), - stepType: 'thinking', - } } - accumulatedThinking += part.text - - // Spec REASONING content event — reasoningMessageId is set in the - // hasEmittedStepStarted block above (entered on the same `part.thought` path) - if (!reasoningMessageId) continue + // Spec REASONING content event yield { type: EventType.REASONING_MESSAGE_CONTENT, messageId: reasoningMessageId, @@ -996,17 +977,6 @@ export class GeminiTextAdapter< model, timestamp: Date.now(), } - - // Legacy STEP event - yield { - type: EventType.STEP_FINISHED, - stepName: stepId || generateId(this.name), - stepId: stepId || generateId(this.name), - model, - timestamp: Date.now(), - delta: part.text, - content: accumulatedThinking, - } } else if (part.text.trim()) { // Close reasoning before text starts if (reasoningMessageId && !hasClosedReasoning) { diff --git a/packages/ai-gemini/src/experimental/text-interactions/adapter.ts b/packages/ai-gemini/src/experimental/text-interactions/adapter.ts index 4da75e0593..351498bd9e 100644 --- a/packages/ai-gemini/src/experimental/text-interactions/adapter.ts +++ b/packages/ai-gemini/src/experimental/text-interactions/adapter.ts @@ -1210,8 +1210,6 @@ async function* translateInteractionEvents( let sawFunctionCall = false const toolCalls = new Map() let nextToolIndex = 0 - let thinkingStepId: string | null = null - let thinkingAccumulated = '' let reasoningMessageId: string | null = null let hasClosedReasoning = false // SDK 2.x routes events by step `index`, not by content id. We need to @@ -1245,7 +1243,6 @@ async function* translateInteractionEvents( // interleaves text → thought → text on some models) opens a // fresh reasoning block instead of re-using an already-ended // messageId, which would violate AG-UI ordering. - thinkingStepId = null reasoningMessageId = null hasClosedReasoning = false } @@ -1379,8 +1376,7 @@ async function* translateInteractionEvents( // `step.delta { thought_summary }` events. If the server // ships a non-empty `summary` array up-front (rare, unary // responses), surface it immediately. - if (thinkingStepId === null || reasoningMessageId === null) { - thinkingStepId = generateId(adapterName) + if (reasoningMessageId === null) { reasoningMessageId = generateId(adapterName) yield { type: EventType.REASONING_START, @@ -1395,18 +1391,9 @@ async function* translateInteractionEvents( model, timestamp, } - yield { - type: EventType.STEP_STARTED, - stepName: thinkingStepId, - stepId: thinkingStepId, - model, - timestamp, - stepType: 'thinking', - } } for (const part of step.summary ?? []) { if (part.type !== 'text' || !part.text) continue - thinkingAccumulated += part.text yield { type: EventType.REASONING_MESSAGE_CONTENT, messageId: reasoningMessageId, @@ -1414,15 +1401,6 @@ async function* translateInteractionEvents( model, timestamp, } - yield { - type: EventType.STEP_FINISHED, - stepName: thinkingStepId, - stepId: thinkingStepId, - model, - timestamp, - delta: part.text, - content: thinkingAccumulated, - } } break } @@ -1631,8 +1609,7 @@ async function* translateInteractionEvents( const thoughtText = delta.content && 'text' in delta.content ? delta.content.text : '' if (!thoughtText) break - if (thinkingStepId === null || reasoningMessageId === null) { - thinkingStepId = generateId(adapterName) + if (reasoningMessageId === null) { reasoningMessageId = generateId(adapterName) yield { type: EventType.REASONING_START, @@ -1647,16 +1624,7 @@ async function* translateInteractionEvents( model, timestamp, } - yield { - type: EventType.STEP_STARTED, - stepName: thinkingStepId, - stepId: thinkingStepId, - model, - timestamp, - stepType: 'thinking', - } } - thinkingAccumulated += thoughtText yield { type: EventType.REASONING_MESSAGE_CONTENT, messageId: reasoningMessageId, @@ -1664,15 +1632,6 @@ async function* translateInteractionEvents( model, timestamp, } - yield { - type: EventType.STEP_FINISHED, - stepName: thinkingStepId, - stepId: thinkingStepId, - model, - timestamp, - delta: thoughtText, - content: thinkingAccumulated, - } break } // The remaining StepDelta variants (image/audio/video/document diff --git a/packages/ai-gemini/src/image/image-provider-options.ts b/packages/ai-gemini/src/image/image-provider-options.ts index 5bcad702c2..2435fe2d79 100644 --- a/packages/ai-gemini/src/image/image-provider-options.ts +++ b/packages/ai-gemini/src/image/image-provider-options.ts @@ -272,8 +272,7 @@ export type GeminiExtendedImageAspectRatio = | '8:1' /** - * Sizes for `gemini-3.1-flash-image` (and its shut-down `-preview` alias): - * all 14 aspect ratios at 512 / 1K / 2K / 4K. `512` is the wire token for the + * Sizes for `gemini-3.1-flash-image`: all 14 aspect ratios at 512 / 1K / 2K / 4K. `512` is the wire token for the * 0.5K tier — not `512px`, and the `K` is case-sensitive (`1k` is rejected). */ export type Gemini31FlashImageSize = @@ -301,21 +300,13 @@ export type GeminiNanoBanana21ImageSize = `${GeminiExtendedImageAspectRatio}_${'1K' | '2K' | '4K'}` /** - * Sizes for `gemini-3-pro-image` (and its shut-down `-preview` alias): the ten - * standard aspect ratios at 1K / 2K / 4K. Pro has no 512 tier and none of the + * Sizes for `gemini-3-pro-image`: the ten standard aspect ratios at 1K / 2K / + * 4K. Pro has no 512 tier and none of the * extreme banner ratios on the Gemini API. */ export type Gemini3ProImageSize = `${GeminiStandardImageAspectRatio}_${'1K' | '2K' | '4K'}` -/** - * Sizes for `gemini-2.5-flash-image`: a bare aspect ratio with no resolution - * suffix, e.g. `'16:9'`. Google documents no `image_size` value or default for - * this model — it emits a single fixed 1024px-class output — so the adapter - * sends `aspect_ratio` and omits `image_size` rather than guessing a tier. - */ -export type Gemini25FlashImageSize = GeminiStandardImageAspectRatio - /** * `imageConfig` fields sent as Interactions `response_format`. * Other `@google/genai` `ImageConfig` keys (`personGeneration`, @@ -336,7 +327,6 @@ export type GeminiNativeImageSize = | Gemini31FlashImageSize | Gemini31FlashLiteImageSize | Gemini3ProImageSize - | Gemini25FlashImageSize /** * Gemini native image models that use the Interactions API path. @@ -359,11 +349,8 @@ export type GeminiNativeImageSize = export const GEMINI_NATIVE_IMAGE_MODELS = [ 'gemini-nano-banana-2.1', 'gemini-3.1-flash-image', - 'gemini-3.1-flash-image-preview', 'gemini-3.1-flash-lite-image', 'gemini-3-pro-image', - 'gemini-3-pro-image-preview', - 'gemini-2.5-flash-image', ] as const satisfies ReadonlyArray export type GeminiNativeImageModels = @@ -394,11 +381,8 @@ export function isGeminiNativeImageModel(model: string): boolean { export type GeminiImageModelSizeByName = { 'gemini-nano-banana-2.1': GeminiNanoBanana21ImageSize 'gemini-3.1-flash-image': Gemini31FlashImageSize - 'gemini-3.1-flash-image-preview': Gemini31FlashImageSize 'gemini-3.1-flash-lite-image': Gemini31FlashLiteImageSize 'gemini-3-pro-image': Gemini3ProImageSize - 'gemini-3-pro-image-preview': Gemini3ProImageSize - 'gemini-2.5-flash-image': Gemini25FlashImageSize } & { [K in Exclude]: GeminiImageSize } @@ -526,10 +510,8 @@ export function validatePrompt(options: { * Format: `"aspectRatio_resolution"`, e.g. `"16:9_4K"` → * `{ aspectRatio: "16:9", resolution: "4K" }`. * - * The resolution suffix is optional: `gemini-2.5-flash-image` takes a bare - * aspect ratio (`"16:9"` → `{ aspectRatio: "16:9" }`) because Google documents - * no `image_size` for it, and the caller must then omit `imageSize` from the - * request rather than substituting a default. + * The resolution suffix is optional: a bare aspect ratio (`"16:9"` → + * `{ aspectRatio: "16:9" }`) leaves `imageSize` out of the request. */ export function parseNativeImageSize( size: string, diff --git a/packages/ai-gemini/src/index.ts b/packages/ai-gemini/src/index.ts index d3fa77de52..6a5916a538 100644 --- a/packages/ai-gemini/src/index.ts +++ b/packages/ai-gemini/src/index.ts @@ -52,7 +52,6 @@ export type { Gemini31FlashImageSize, Gemini31FlashLiteImageSize, Gemini3ProImageSize, - Gemini25FlashImageSize, GeminiNativeImageSize, // Re-export SDK types for convenience PersonGeneration, diff --git a/packages/ai-gemini/src/model-meta.ts b/packages/ai-gemini/src/model-meta.ts index 449a165352..daa697490c 100644 --- a/packages/ai-gemini/src/model-meta.ts +++ b/packages/ai-gemini/src/model-meta.ts @@ -173,39 +173,6 @@ const GEMINI_3_PRO_IMAGE = { GeminiStructuredOutputOptions > -/** - * @deprecated `gemini-3-pro-image-preview` was shut down on 2026-06-25. Use - * the GA id `gemini-3-pro-image` instead — the preview id now 404s. - * Kept in the model union so existing code still compiles. - * @see https://ai.google.dev/gemini-api/docs/deprecations - */ -const GEMINI_3_PRO_IMAGE_PREVIEW = { - name: 'gemini-3-pro-image-preview', - max_input_tokens: 65_536, - max_output_tokens: 32_768, - knowledge_cutoff: '2025-01-01', - supports: { - input: ['text', 'image'], - output: ['text', 'image'], - capabilities: ['batch_api', 'structured_output', 'thinking'], - tools: ['google_search'], - }, - pricing: { - input: { - normal: 2, - }, - output: { - normal: 0.134, - }, - }, -} as const satisfies ModelMeta< - GeminiToolConfigOptions & - GeminiSafetyOptions & - GeminiCommonConfigOptions & - GeminiCachedContentOptions & - GeminiStructuredOutputOptions -> - /** * Gemini 3.1 Flash Image ("Nano Banana 2") — GA. The only native image model * that accepts the four extreme banner ratios (1:4, 4:1, 1:8, 8:1) and the @@ -238,39 +205,6 @@ const GEMINI_3_1_FLASH_IMAGE = { GeminiCachedContentOptions > -/** - * @deprecated `gemini-3.1-flash-image-preview` was shut down on 2026-06-25. - * Use the GA id `gemini-3.1-flash-image` instead — the preview id now 404s. - * Kept in the model union so existing code still compiles. - * @see https://ai.google.dev/gemini-api/docs/deprecations - */ -const GEMINI_3_1_FLASH_IMAGE_PREVIEW = { - name: 'gemini-3.1-flash-image-preview', - max_input_tokens: 65_536, - max_output_tokens: 65_536, - knowledge_cutoff: '2025-01-01', - supports: { - input: ['text', 'image'], - output: ['text', 'image'], - capabilities: ['batch_api', 'structured_output', 'thinking'], - tools: ['google_search'], - }, - pricing: { - input: { - normal: 0.25, - }, - output: { - normal: 1.5, - }, - }, -} as const satisfies ModelMeta< - GeminiToolConfigOptions & - GeminiSafetyOptions & - GeminiCommonConfigOptions & - GeminiCachedContentOptions & - GeminiStructuredOutputOptions -> - /** * Gemini 3.1 Flash Lite Image ("Nano Banana 2 Lite") — GA. 1K output only. * @see https://ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-lite-image @@ -506,42 +440,6 @@ const GEMINI_2_5_FLASH = { GeminiStructuredOutputOptions > -/** - * Gemini 2.5 Flash Image ("Nano Banana") — still GA, but documented as the - * legacy member of the family. Google publishes no `image_size` value for it, - * so its size type is a bare aspect ratio and the adapter sends no - * `imageConfig.imageSize`. - * @deprecated `gemini-2.5-flash-image` shuts down on 2026-10-02. Migrate to - * `gemini-3.1-flash-lite-image` (cheapest successor) or - * `gemini-3.1-flash-image`. Google's deprecations table still names the - * already-dead `gemini-3.1-flash-image-preview` as the replacement. - * @see https://ai.google.dev/gemini-api/docs/deprecations - */ -const GEMINI_2_5_FLASH_IMAGE = { - name: 'gemini-2.5-flash-image', - max_input_tokens: 1_048_576, - max_output_tokens: 65_536, - knowledge_cutoff: '2025-06-01', - supports: { - input: ['text', 'image'], - output: ['text', 'image'], - capabilities: ['batch_api', 'caching', 'structured_output'], - tools: ['file_search'], - }, - pricing: { - input: { - normal: 0.3, - }, - output: { - normal: 0.4, - }, - }, -} as const satisfies ModelMeta< - GeminiToolConfigOptions & - GeminiSafetyOptions & - GeminiCommonConfigOptions & - GeminiCachedContentOptions -> /** const GEMINI_2_5_FLASH_LIVE = { name: 'gemini-2.5-flash-native-audio-preview-09-2025', @@ -1138,41 +1036,19 @@ export const GEMINI_COMBINED_TOOLS_AND_SCHEMA_MODELS = new Set([ export type GeminiModels = (typeof GEMINI_MODELS)[number] -/** - * @deprecated Shut down 2026-06-25. Use `gemini-3.1-flash-image`. - */ -type Gemini31FlashImagePreviewModel = 'gemini-3.1-flash-image-preview' - -/** - * @deprecated Shut down 2026-06-25. Use `gemini-3-pro-image`. - */ -type Gemini3ProImagePreviewModel = 'gemini-3-pro-image-preview' - -export type GeminiImageModels = - | Exclude< - (typeof GEMINI_IMAGE_MODELS)[number], - Gemini31FlashImagePreviewModel | Gemini3ProImagePreviewModel - > - | Gemini31FlashImagePreviewModel - | Gemini3ProImagePreviewModel +export type GeminiImageModels = (typeof GEMINI_IMAGE_MODELS)[number] /** - * Image generation models. GA ids come first; the trailing `-preview` ids are - * shut-down aliases kept only so existing code keeps compiling — new code - * should use the GA id above its alias. + * Image generation models. */ export const GEMINI_IMAGE_MODELS = [ GEMINI_NANO_BANANA_2_1.name, GEMINI_3_1_FLASH_IMAGE.name, GEMINI_3_1_FLASH_LITE_IMAGE.name, GEMINI_3_PRO_IMAGE.name, - GEMINI_2_5_FLASH_IMAGE.name, IMAGEN_4_GENERATE.name, IMAGEN_4_GENERATE_FAST.name, IMAGEN_4_GENERATE_ULTRA.name, - // Deprecated aliases — shut down 2026-06-25. - GEMINI_3_1_FLASH_IMAGE_PREVIEW.name, - GEMINI_3_PRO_IMAGE_PREVIEW.name, ] as const /** diff --git a/packages/ai-gemini/src/tools/code-execution-tool.ts b/packages/ai-gemini/src/tools/code-execution-tool.ts index bd2b1f3ecd..f338921985 100644 --- a/packages/ai-gemini/src/tools/code-execution-tool.ts +++ b/packages/ai-gemini/src/tools/code-execution-tool.ts @@ -3,9 +3,6 @@ import type { ProviderTool, Tool } from '@tanstack/ai' export interface CodeExecutionToolConfig {} -/** @deprecated Renamed to `CodeExecutionToolConfig`. Will be removed in a future release. */ -export type CodeExecutionTool = CodeExecutionToolConfig - export type GeminiCodeExecutionTool = ProviderTool<'gemini', 'code_execution'> export function convertCodeExecutionToolToAdapterFormat(_tool: Tool) { diff --git a/packages/ai-gemini/src/tools/computer-use-tool.ts b/packages/ai-gemini/src/tools/computer-use-tool.ts index 48b2dc0e28..78dfda7b11 100644 --- a/packages/ai-gemini/src/tools/computer-use-tool.ts +++ b/packages/ai-gemini/src/tools/computer-use-tool.ts @@ -7,9 +7,6 @@ import type { ProviderTool, Tool } from '@tanstack/ai' export type ComputerUseToolConfig = ComputerUse -/** @deprecated Renamed to `ComputerUseToolConfig`. Will be removed in a future release. */ -export type ComputerUseTool = ComputerUseToolConfig - export type GeminiComputerUseTool = ProviderTool<'gemini', 'computer_use'> export function convertComputerUseToolToAdapterFormat(tool: Tool) { diff --git a/packages/ai-gemini/src/tools/file-search-tool.ts b/packages/ai-gemini/src/tools/file-search-tool.ts index 0d38144aef..104d0ccb06 100644 --- a/packages/ai-gemini/src/tools/file-search-tool.ts +++ b/packages/ai-gemini/src/tools/file-search-tool.ts @@ -7,9 +7,6 @@ import type { ProviderTool, Tool } from '@tanstack/ai' export type FileSearchToolConfig = FileSearch -/** @deprecated Renamed to `FileSearchToolConfig`. Will be removed in a future release. */ -export type FileSearchTool = FileSearchToolConfig - export type GeminiFileSearchTool = ProviderTool<'gemini', 'file_search'> export function convertFileSearchToolToAdapterFormat(tool: Tool) { diff --git a/packages/ai-gemini/src/tools/google-maps-tool.ts b/packages/ai-gemini/src/tools/google-maps-tool.ts index 287adc3577..559f151025 100644 --- a/packages/ai-gemini/src/tools/google-maps-tool.ts +++ b/packages/ai-gemini/src/tools/google-maps-tool.ts @@ -7,9 +7,6 @@ import type { ProviderTool, Tool } from '@tanstack/ai' export type GoogleMapsToolConfig = GoogleMaps -/** @deprecated Renamed to `GoogleMapsToolConfig`. Will be removed in a future release. */ -export type GoogleMapsTool = GoogleMapsToolConfig - export type GeminiGoogleMapsTool = ProviderTool<'gemini', 'google_maps'> export function convertGoogleMapsToolToAdapterFormat(tool: Tool) { diff --git a/packages/ai-gemini/src/tools/google-search-retriveal-tool.ts b/packages/ai-gemini/src/tools/google-search-retriveal-tool.ts index f87e5327e4..536457d8e1 100644 --- a/packages/ai-gemini/src/tools/google-search-retriveal-tool.ts +++ b/packages/ai-gemini/src/tools/google-search-retriveal-tool.ts @@ -7,9 +7,6 @@ import type { ProviderTool, Tool } from '@tanstack/ai' export type GoogleSearchRetrievalToolConfig = GoogleSearchRetrieval -/** @deprecated Renamed to `GoogleSearchRetrievalToolConfig`. Will be removed in a future release. */ -export type GoogleSearchRetrievalTool = GoogleSearchRetrievalToolConfig - export type GeminiGoogleSearchRetrievalTool = ProviderTool< 'gemini', 'google_search_retrieval' diff --git a/packages/ai-gemini/src/tools/google-search-tool.ts b/packages/ai-gemini/src/tools/google-search-tool.ts index 0c645831a8..1f03613279 100644 --- a/packages/ai-gemini/src/tools/google-search-tool.ts +++ b/packages/ai-gemini/src/tools/google-search-tool.ts @@ -7,9 +7,6 @@ import type { ProviderTool, Tool } from '@tanstack/ai' export type GoogleSearchToolConfig = GoogleSearch -/** @deprecated Renamed to `GoogleSearchToolConfig`. Will be removed in a future release. */ -export type GoogleSearchTool = GoogleSearchToolConfig - export type GeminiGoogleSearchTool = ProviderTool<'gemini', 'google_search'> export function convertGoogleSearchToolToAdapterFormat(tool: Tool) { diff --git a/packages/ai-gemini/src/tools/index.ts b/packages/ai-gemini/src/tools/index.ts index d39a396d78..8fed6c20f0 100644 --- a/packages/ai-gemini/src/tools/index.ts +++ b/packages/ai-gemini/src/tools/index.ts @@ -1,29 +1,26 @@ -import type { CodeExecutionTool } from './code-execution-tool' -import type { ComputerUseTool } from './computer-use-tool' -import type { FileSearchTool } from './file-search-tool' +import type { CodeExecutionToolConfig } from './code-execution-tool' +import type { ComputerUseToolConfig } from './computer-use-tool' +import type { FileSearchToolConfig } from './file-search-tool' import type { FunctionDeclarationTool } from './function-declaration-tool' -import type { GoogleMapsTool } from './google-maps-tool' -import type { GoogleSearchRetrievalTool } from './google-search-retriveal-tool' -import type { GoogleSearchTool } from './google-search-tool' -import type { UrlContextTool } from './url-context-tool' +import type { GoogleMapsToolConfig } from './google-maps-tool' +import type { GoogleSearchRetrievalToolConfig } from './google-search-retriveal-tool' +import type { GoogleSearchToolConfig } from './google-search-tool' +import type { UrlContextToolConfig } from './url-context-tool' export { codeExecutionTool, type GeminiCodeExecutionTool, type CodeExecutionToolConfig, - type CodeExecutionTool, } from './code-execution-tool' export { computerUseTool, type GeminiComputerUseTool, type ComputerUseToolConfig, - type ComputerUseTool, } from './computer-use-tool' export { fileSearchTool, type GeminiFileSearchTool, type FileSearchToolConfig, - type FileSearchTool, } from './file-search-tool' export { functionDeclarationTools, @@ -33,37 +30,32 @@ export { googleMapsTool, type GeminiGoogleMapsTool, type GoogleMapsToolConfig, - type GoogleMapsTool, } from './google-maps-tool' export { googleSearchRetrievalTool, type GeminiGoogleSearchRetrievalTool, type GoogleSearchRetrievalToolConfig, - type GoogleSearchRetrievalTool, } from './google-search-retriveal-tool' export { googleSearchTool, type GeminiGoogleSearchTool, type GoogleSearchToolConfig, - type GoogleSearchTool, } from './google-search-tool' export { urlContextTool, type GeminiUrlContextTool, type UrlContextToolConfig, - type UrlContextTool, } from './url-context-tool' // Keep the existing discriminated union defined inline (no barrel exports). -// Built from the deprecated config-type aliases — matches the SDK-adapter shape. export type GoogleGeminiTool = - | CodeExecutionTool - | ComputerUseTool - | FileSearchTool + | CodeExecutionToolConfig + | ComputerUseToolConfig + | FileSearchToolConfig | FunctionDeclarationTool - | GoogleMapsTool - | GoogleSearchRetrievalTool - | GoogleSearchTool - | UrlContextTool + | GoogleMapsToolConfig + | GoogleSearchRetrievalToolConfig + | GoogleSearchToolConfig + | UrlContextToolConfig export { convertToolsToProviderFormat } from './tool-converter' diff --git a/packages/ai-gemini/src/tools/url-context-tool.ts b/packages/ai-gemini/src/tools/url-context-tool.ts index e162e49cef..08313fe7e7 100644 --- a/packages/ai-gemini/src/tools/url-context-tool.ts +++ b/packages/ai-gemini/src/tools/url-context-tool.ts @@ -3,9 +3,6 @@ import type { ProviderTool, Tool } from '@tanstack/ai' export interface UrlContextToolConfig {} -/** @deprecated Renamed to `UrlContextToolConfig`. Will be removed in a future release. */ -export type UrlContextTool = UrlContextToolConfig - export type GeminiUrlContextTool = ProviderTool<'gemini', 'url_context'> export function convertUrlContextToolToAdapterFormat(_tool: Tool) { diff --git a/packages/ai-gemini/tests/image-adapter.test.ts b/packages/ai-gemini/tests/image-adapter.test.ts index 136ba33f2b..0db00c3759 100644 --- a/packages/ai-gemini/tests/image-adapter.test.ts +++ b/packages/ai-gemini/tests/image-adapter.test.ts @@ -43,10 +43,7 @@ function interactionWithImages( */ function mockedNativeAdapter() { const mockCreate = vi.fn().mockResolvedValueOnce(interactionWithImages('out')) - const adapter = createGeminiImage( - 'gemini-3.1-flash-image-preview', - 'test-api-key', - ) + const adapter = createGeminiImage('gemini-3.1-flash-image', 'test-api-key') ;( adapter as unknown as { client: { interactions: { create: unknown } } @@ -254,8 +251,7 @@ describe('Gemini Image Adapter', () => { aspectRatio: '1:8', resolution: '512', }) - // gemini-2.5-flash-image has no documented image_size, so its sizes are - // bare ratios: the resolution is absent, not defaulted. + // A bare ratio has no resolution: it is absent, not defaulted. expect(parseNativeImageSize('16:9')).toEqual({ aspectRatio: '16:9' }) expect(parseNativeImageSize('16:9')?.resolution).toBeUndefined() }) @@ -360,7 +356,7 @@ describe('Gemini Image Adapter', () => { .fn() .mockResolvedValueOnce(interactionWithImages('gemini-base64-image')) const adapter = createGeminiImage( - 'gemini-3.1-flash-image-preview', + 'gemini-3.1-flash-image', 'test-api-key', ) stubCreate(adapter, mockCreate) @@ -372,7 +368,7 @@ describe('Gemini Image Adapter', () => { }) expect(mockCreate).toHaveBeenCalledWith({ - model: 'gemini-3.1-flash-image-preview', + model: 'gemini-3.1-flash-image', input: 'A futuristic city', stream: false, response_format: { @@ -383,7 +379,7 @@ describe('Gemini Image Adapter', () => { }) expect(result.id).toBe('int_test') - expect(result.model).toBe('gemini-3.1-flash-image-preview') + expect(result.model).toBe('gemini-3.1-flash-image') expect(result.images).toHaveLength(1) expect(result.images[0]!.b64Json).toBe('gemini-base64-image') }) @@ -511,38 +507,6 @@ describe('Gemini Image Adapter', () => { expect(result.images[0]!.b64Json).toBe('ga-pro-image') }) - it('sends aspect_ratio but no image_size for gemini-2.5-flash-image', async () => { - // Google documents no image_size for this model, so the adapter must - // send the ratio alone rather than inventing a resolution tier. - const mockCreate = vi - .fn() - .mockResolvedValueOnce(interactionWithImages('legacy-image')) - const adapter = createGeminiImage( - 'gemini-2.5-flash-image', - 'test-api-key', - ) - stubCreate(adapter, mockCreate) - - await generateImage({ - adapter, - prompt: 'A wide landscape', - size: '16:9', - }) - - expect(mockCreate).toHaveBeenCalledWith({ - model: 'gemini-2.5-flash-image', - input: 'A wide landscape', - stream: false, - response_format: { - type: 'image', - aspect_ratio: '16:9', - }, - }) - - const format = mockCreate.mock.calls[0]![0].response_format - expect('image_size' in format).toBe(false) - }) - it('surfaces token usage from the interaction (#330)', async () => { const mockCreate = vi.fn().mockResolvedValueOnce( interactionWithImages('img', { @@ -554,7 +518,7 @@ describe('Gemini Image Adapter', () => { }), ) const adapter = createGeminiImage( - 'gemini-3.1-flash-image-preview', + 'gemini-3.1-flash-image', 'test-api-key', ) stubCreate(adapter, mockCreate) @@ -576,7 +540,7 @@ describe('Gemini Image Adapter', () => { .fn() .mockResolvedValueOnce(interactionWithImages('gemini-base64-image')) const adapter = createGeminiImage( - 'gemini-3.1-flash-image-preview', + 'gemini-3.1-flash-image', 'test-api-key', ) stubCreate(adapter, mockCreate) @@ -587,7 +551,7 @@ describe('Gemini Image Adapter', () => { }) expect(mockCreate).toHaveBeenCalledWith({ - model: 'gemini-3.1-flash-image-preview', + model: 'gemini-3.1-flash-image', input: 'A simple sketch', stream: false, response_format: { type: 'image' }, @@ -603,7 +567,7 @@ describe('Gemini Image Adapter', () => { steps: [{ type: 'model_output', content: [] }], }) const adapter = createGeminiImage( - 'gemini-3.1-flash-image-preview', + 'gemini-3.1-flash-image', 'test-api-key', ) stubCreate(adapter, mockCreate) @@ -628,7 +592,7 @@ describe('Gemini Image Adapter', () => { ], }) const adapter = createGeminiImage( - 'gemini-3.1-flash-image-preview', + 'gemini-3.1-flash-image', 'test-api-key', ) stubCreate(adapter, mockCreate) @@ -675,7 +639,7 @@ describe('Gemini Image Adapter', () => { ], }) const adapter = createGeminiImage( - 'gemini-3.1-flash-image-preview', + 'gemini-3.1-flash-image', 'test-api-key', ) stubCreate(adapter, mockCreate) @@ -687,7 +651,7 @@ describe('Gemini Image Adapter', () => { }) expect(mockCreate).toHaveBeenCalledWith({ - model: 'gemini-3.1-flash-image-preview', + model: 'gemini-3.1-flash-image', input: 'A futuristic city Generate 3 distinct images.', stream: false, response_format: { type: 'image' }, @@ -708,7 +672,7 @@ describe('Gemini Image Adapter', () => { }) expect(mockCreate).toHaveBeenCalledWith({ - model: 'gemini-3.1-flash-image-preview', + model: 'gemini-3.1-flash-image', input: 'A simple sketch', stream: false, response_format: { type: 'image' }, @@ -724,7 +688,7 @@ describe('Gemini Image Adapter', () => { }) expect(mockCreate).toHaveBeenCalledWith({ - model: 'gemini-3.1-flash-image-preview', + model: 'gemini-3.1-flash-image', input: 'A simple sketch', stream: false, response_format: { type: 'image' }, @@ -854,7 +818,7 @@ describe('Gemini Image Adapter', () => { }) expect(mockCreate).toHaveBeenCalledWith({ - model: 'gemini-3.1-flash-image-preview', + model: 'gemini-3.1-flash-image', input: 'Make the water greener', stream: false, response_format: { type: 'image' }, @@ -870,7 +834,7 @@ describe('Gemini Image Adapter', () => { const logger = resolveDebugOption(false) await adapter.generateImages({ - model: 'gemini-3.1-flash-image-preview', + model: 'gemini-3.1-flash-image', prompt: 'A quiet harbour', logger, abortSignal: controller.signal, @@ -878,7 +842,7 @@ describe('Gemini Image Adapter', () => { expect(mockCreate).toHaveBeenCalledWith( { - model: 'gemini-3.1-flash-image-preview', + model: 'gemini-3.1-flash-image', input: 'A quiet harbour', stream: false, response_format: { type: 'image' }, @@ -1036,7 +1000,7 @@ describe('Gemini Image Adapter', () => { }) expect(mockCreate).toHaveBeenCalledWith({ - model: 'gemini-3.1-flash-image-preview', + model: 'gemini-3.1-flash-image', input: [ { type: 'text', text: 'Not like this' }, { type: 'image', data: 'YmFk', mime_type: 'image/jpeg' }, @@ -1120,7 +1084,7 @@ describe('Gemini Image Adapter', () => { ], }) const adapter = createGeminiImage( - 'gemini-3.1-flash-image-preview', + 'gemini-3.1-flash-image', 'test-api-key', ) stubCreate(adapter, mockCreate) @@ -1151,7 +1115,7 @@ describe('Gemini Image Adapter', () => { ], }) const adapter = createGeminiImage( - 'gemini-3.1-flash-image-preview', + 'gemini-3.1-flash-image', 'test-api-key', ) stubCreate(adapter, mockCreate) @@ -1171,7 +1135,7 @@ describe('Gemini Image Adapter', () => { steps: [], }) const adapter = createGeminiImage( - 'gemini-3.1-flash-image-preview', + 'gemini-3.1-flash-image', 'test-api-key', ) stubCreate(adapter, mockCreate) @@ -1204,13 +1168,13 @@ describe('Gemini Image Adapter', () => { it('rejects video and audio prompt parts', async () => { const adapter = createGeminiImage( - 'gemini-3.1-flash-image-preview', + 'gemini-3.1-flash-image', 'test-api-key', ) await expect( adapter.generateImages({ - model: 'gemini-3.1-flash-image-preview', + model: 'gemini-3.1-flash-image', prompt: [ { type: 'text', content: 'x' }, { @@ -1224,7 +1188,7 @@ describe('Gemini Image Adapter', () => { await expect( adapter.generateImages({ - model: 'gemini-3.1-flash-image-preview', + model: 'gemini-3.1-flash-image', prompt: [ { type: 'text', content: 'x' }, { @@ -1244,7 +1208,6 @@ describe('GEMINI_NATIVE_IMAGE_MODELS public routing list', () => { expect(isGeminiNativeImageModel('gemini-3.1-flash-image')).toBe(true) expect(isGeminiNativeImageModel('gemini-nano-banana-2.1')).toBe(true) expect(isGeminiNativeImageModel('gemini-3-pro-image')).toBe(true) - expect(isGeminiNativeImageModel('gemini-2.5-flash-image')).toBe(true) expect(isGeminiNativeImageModel('imagen-4.0-generate-001')).toBe(false) expect(isGeminiNativeImageModel('gemini-9-pro-image')).toBe(false) expect(GEMINI_NATIVE_IMAGE_MODELS).toContain('gemini-3.1-flash-image') diff --git a/packages/ai-gemini/tests/image-per-model-type-safety.test.ts b/packages/ai-gemini/tests/image-per-model-type-safety.test.ts index 7fdfb2aec6..8d3bc4ecd9 100644 --- a/packages/ai-gemini/tests/image-per-model-type-safety.test.ts +++ b/packages/ai-gemini/tests/image-per-model-type-safety.test.ts @@ -63,14 +63,6 @@ describe('Gemini per-model image size gating', () => { size: '1:1_1k', }) }) - - it('applies the same set to the shut-down -preview alias', () => { - createImageOptions({ - adapter: createGeminiImage('gemini-3.1-flash-image-preview', apiKey), - prompt: 'a wide banner', - size: '1:8_512', - }) - }) }) describe('gemini-nano-banana-2.1 — 14 ratios × 1K/2K/4K', () => { @@ -165,53 +157,6 @@ describe('Gemini per-model image size gating', () => { size: '1:1_512', }) }) - - it('applies the same set to the shut-down -preview alias', () => { - createImageOptions({ - adapter: createGeminiImage('gemini-3-pro-image-preview', apiKey), - prompt: 'a portrait', - size: '4:5_2K', - }) - }) - }) - - describe('gemini-2.5-flash-image — bare aspect ratio, no resolution', () => { - it('accepts a bare ratio', () => { - createImageOptions({ - adapter: createGeminiImage('gemini-2.5-flash-image', apiKey), - prompt: 'a landscape', - size: '16:9', - }) - createImageOptions({ - adapter: createGeminiImage('gemini-2.5-flash-image', apiKey), - prompt: 'a portrait', - size: '4:5', - }) - }) - - it('rejects any resolution suffix', () => { - createImageOptions({ - adapter: createGeminiImage('gemini-2.5-flash-image', apiKey), - prompt: 'a landscape', - // @ts-expect-error - Google documents no image_size for this model - size: '16:9_2K', - }) - createImageOptions({ - adapter: createGeminiImage('gemini-2.5-flash-image', apiKey), - prompt: 'a landscape', - // @ts-expect-error - Google documents no image_size for this model - size: '16:9_1K', - }) - }) - - it('rejects the extreme banner ratios', () => { - createImageOptions({ - adapter: createGeminiImage('gemini-2.5-flash-image', apiKey), - prompt: 'a wide strip', - // @ts-expect-error - 8:1 is Gemini 3.1 Flash Image only - size: '8:1', - }) - }) }) describe('Imagen models keep pixel sizes', () => { @@ -239,20 +184,7 @@ describe('Gemini per-model image size gating', () => { }) describe('Gemini image size map shape assertions', () => { - it('maps each GA id to its own size set', () => { - expectTypeOf< - GeminiImageModelSizeByName['gemini-3.1-flash-image'] - >().toEqualTypeOf< - GeminiImageModelSizeByName['gemini-3.1-flash-image-preview'] - >() - expectTypeOf< - GeminiImageModelSizeByName['gemini-3-pro-image'] - >().toEqualTypeOf< - GeminiImageModelSizeByName['gemini-3-pro-image-preview'] - >() - }) - - it('gives the four native models four different size sets', () => { + it('gives the native models different size sets', () => { expectTypeOf< GeminiImageModelSizeByName['gemini-3.1-flash-image'] >().not.toEqualTypeOf< @@ -261,7 +193,7 @@ describe('Gemini image size map shape assertions', () => { expectTypeOf< GeminiImageModelSizeByName['gemini-3-pro-image'] >().not.toEqualTypeOf< - GeminiImageModelSizeByName['gemini-2.5-flash-image'] + GeminiImageModelSizeByName['gemini-nano-banana-2.1'] >() }) }) @@ -273,10 +205,10 @@ beforeAll(() => { }) describe('Gemini per-model image modelOptions gating', () => { - describe('gemini-3.1-flash-image-preview — native (Interactions API)', () => { + describe('gemini-3.1-flash-image — native (Interactions API)', () => { it('accepts the native option set', () => { createImageOptions({ - adapter: geminiImage('gemini-3.1-flash-image-preview'), + adapter: geminiImage('gemini-3.1-flash-image'), prompt: 'a quiet harbour', modelOptions: { seed: 7, @@ -292,7 +224,7 @@ describe('Gemini per-model image modelOptions gating', () => { it('rejects Vertex-only imageConfig fields', () => { createImageOptions({ - adapter: geminiImage('gemini-3.1-flash-image-preview'), + adapter: geminiImage('gemini-3.1-flash-image'), prompt: 'a quiet harbour', modelOptions: { imageConfig: { @@ -311,7 +243,7 @@ describe('Gemini per-model image modelOptions gating', () => { // `personGeneration` would reject a string literal on either shape and // would still "pass" with the split reverted. createImageOptions({ - adapter: geminiImage('gemini-3.1-flash-image-preview'), + adapter: geminiImage('gemini-3.1-flash-image'), prompt: 'a quiet harbour', modelOptions: { // @ts-expect-error - negativePrompt is a GenerateImagesConfig (Imagen) field @@ -320,7 +252,7 @@ describe('Gemini per-model image modelOptions gating', () => { }) createImageOptions({ - adapter: geminiImage('gemini-3.1-flash-image-preview'), + adapter: geminiImage('gemini-3.1-flash-image'), prompt: 'a quiet harbour', modelOptions: { // @ts-expect-error - aspectRatio is Imagen-only; native models use size / imageConfig @@ -331,7 +263,7 @@ describe('Gemini per-model image modelOptions gating', () => { it('rejects responseModalities — the adapter owns it', () => { createImageOptions({ - adapter: geminiImage('gemini-3.1-flash-image-preview'), + adapter: geminiImage('gemini-3.1-flash-image'), prompt: 'a quiet harbour', modelOptions: { // @ts-expect-error - responseModalities is a protected adapter default @@ -380,7 +312,7 @@ describe('Gemini per-model image modelOptions gating', () => { describe('Gemini image provider options shape assertions', () => { describe('native models take Interactions image fields', () => { type Options = - GeminiImageModelProviderOptionsByName['gemini-3.1-flash-image-preview'] + GeminiImageModelProviderOptionsByName['gemini-3.1-flash-image'] it('has safetySettings', () => { expectTypeOf().toHaveProperty('safetySettings') @@ -436,15 +368,5 @@ describe('Gemini image provider options shape assertions', () => { GeminiImageModelProviderOptionsByName['gemini-3.1-flash-lite-image'] >().toHaveProperty('imageConfig') }) - it('gemini-3-pro-image-preview', () => { - expectTypeOf< - GeminiImageModelProviderOptionsByName['gemini-3-pro-image-preview'] - >().toHaveProperty('imageConfig') - }) - it('gemini-2.5-flash-image', () => { - expectTypeOf< - GeminiImageModelProviderOptionsByName['gemini-2.5-flash-image'] - >().toHaveProperty('imageConfig') - }) }) }) diff --git a/packages/ai-grok/src/adapters/transcription.ts b/packages/ai-grok/src/adapters/transcription.ts index e3048192c2..8037b7eef8 100644 --- a/packages/ai-grok/src/adapters/transcription.ts +++ b/packages/ai-grok/src/adapters/transcription.ts @@ -146,7 +146,6 @@ export class GrokTranscriptionAdapter< completionTokens: 0, totalTokens: 0, billed: { quantity: data.duration, unit: 'seconds' }, - durationSeconds: data.duration, } : undefined return { diff --git a/packages/ai-grok/src/adapters/video.ts b/packages/ai-grok/src/adapters/video.ts index 22bd8822fd..07a9c94c5e 100644 --- a/packages/ai-grok/src/adapters/video.ts +++ b/packages/ai-grok/src/adapters/video.ts @@ -94,7 +94,6 @@ function buildGrokVideoUsage( totalTokens: 0, ...(seconds !== undefined && { billed: { quantity: seconds, unit: 'seconds' }, - unitsBilled: seconds, }), ...(ticks !== undefined && { cost: ticks / USD_TICKS_PER_DOLLAR }), } diff --git a/packages/ai-grok/src/model-meta.ts b/packages/ai-grok/src/model-meta.ts index bf6b5a3412..217764cc21 100644 --- a/packages/ai-grok/src/model-meta.ts +++ b/packages/ai-grok/src/model-meta.ts @@ -378,17 +378,6 @@ const GROK_VOICE_FAST_1 = { }, } as const satisfies ModelMeta -/** @deprecated xAI has deprecated grok-voice-think-fast-1.0 — use grok-voice-think-fast-2.0. */ -const GROK_VOICE_THINK_FAST_1 = { - name: 'grok-voice-think-fast-1.0', - supports: { - input: ['audio', 'text'], - output: ['audio', 'text'], - capabilities: ['reasoning', 'tool_calling'], - tools: [] as const, - }, -} as const satisfies ModelMeta - // xAI's current recommended speech-to-speech model. const GROK_VOICE_THINK_FAST_2 = { name: 'grok-voice-think-fast-2.0', @@ -420,7 +409,6 @@ export const GROK_REALTIME_MODELS = [ GROK_VOICE_THINK_FAST_2.name, GROK_VOICE_LATEST.name, GROK_VOICE_FAST_1.name, - GROK_VOICE_THINK_FAST_1.name, ] as const /** diff --git a/packages/ai-grok/tests/audio-adapters.test.ts b/packages/ai-grok/tests/audio-adapters.test.ts index 42602af672..db279e72f3 100644 --- a/packages/ai-grok/tests/audio-adapters.test.ts +++ b/packages/ai-grok/tests/audio-adapters.test.ts @@ -307,7 +307,6 @@ describe('GrokTranscriptionAdapter', () => { completionTokens: 0, totalTokens: 0, billed: { quantity: 1.23, unit: 'seconds' }, - durationSeconds: 1.23, }) // Grok returns `confidence` per word when the model provides one; we // surface it under `GrokTranscriptionWord` so callers that know they're diff --git a/packages/ai-grok/tests/realtime-token.test.ts b/packages/ai-grok/tests/realtime-token.test.ts index e8c291d237..785a59a3fc 100644 --- a/packages/ai-grok/tests/realtime-token.test.ts +++ b/packages/ai-grok/tests/realtime-token.test.ts @@ -56,17 +56,17 @@ describe('grokRealtimeToken request body', () => { const fetchMock = vi .fn() .mockResolvedValue( - makeSessionResponse(1_700_000_000, 'grok-voice-think-fast-1.0'), + makeSessionResponse(1_700_000_000, 'grok-voice-fast-1.0'), ) globalThis.fetch = fetchMock as unknown as typeof fetch await realtimeToken({ - adapter: grokRealtimeToken({ model: 'grok-voice-think-fast-1.0' }), + adapter: grokRealtimeToken({ model: 'grok-voice-fast-1.0' }), }) const init = fetchMock.mock.calls[0]![1]! const body = JSON.parse(init.body as string) as Record - expect(body).toEqual({ session: { model: 'grok-voice-think-fast-1.0' } }) + expect(body).toEqual({ session: { model: 'grok-voice-fast-1.0' } }) }) it("defaults to xAI's current recommended model when no model is given", async () => { diff --git a/packages/ai-grok/tests/video-adapter.test.ts b/packages/ai-grok/tests/video-adapter.test.ts index 217c13c91f..2f3a5df487 100644 --- a/packages/ai-grok/tests/video-adapter.test.ts +++ b/packages/ai-grok/tests/video-adapter.test.ts @@ -1271,7 +1271,6 @@ describe('Grok Video Adapter', () => { completionTokens: 0, totalTokens: 0, billed: { quantity: 5, unit: 'seconds' }, - unitsBilled: 5, cost: 0.25, }, }) diff --git a/packages/ai-octane/src/chat-ui/create-chat-hook.tsrx b/packages/ai-octane/src/chat-ui/create-chat-hook.tsrx index d458fbf58b..b7a4124758 100644 --- a/packages/ai-octane/src/chat-ui/create-chat-hook.tsrx +++ b/packages/ai-octane/src/chat-ui/create-chat-hook.tsrx @@ -24,7 +24,6 @@ type ChatInstanceOverrides = { threadId?: string live?: boolean forwardedProps?: Record - body?: Record initialMessages?: HeadlessOptions['initialMessages'] } diff --git a/packages/ai-octane/src/chat-ui/create-chat-hook.tsrx.d.ts b/packages/ai-octane/src/chat-ui/create-chat-hook.tsrx.d.ts index 1e566c92f5..85d573b5db 100644 --- a/packages/ai-octane/src/chat-ui/create-chat-hook.tsrx.d.ts +++ b/packages/ai-octane/src/chat-ui/create-chat-hook.tsrx.d.ts @@ -13,7 +13,6 @@ export declare function createChatHook< threadId?: string live?: boolean forwardedProps?: Record - body?: Record initialMessages?: ChatUIHost['messages'] }) => ChatUIHost & { AppChat: ComponentBody } useChatContext: () => ChatUIHost diff --git a/packages/ai-octane/src/create-chat-hook.ts b/packages/ai-octane/src/create-chat-hook.ts index d8d2c291d7..12bf0f0025 100644 --- a/packages/ai-octane/src/create-chat-hook.ts +++ b/packages/ai-octane/src/create-chat-hook.ts @@ -16,7 +16,6 @@ type ChatHookOverrides< threadId?: string live?: boolean forwardedProps?: Record - body?: Record initialMessages?: UseChatOptions< TTools, TSchema, diff --git a/packages/ai-octane/src/index.ts b/packages/ai-octane/src/index.ts index 6dc539e753..98f4deaafc 100644 --- a/packages/ai-octane/src/index.ts +++ b/packages/ai-octane/src/index.ts @@ -6,12 +6,10 @@ export type { UseMcpAppBridgeOptions } from './use-mcp-app-bridge.tsrx' export { usePageWebMCPTools, useRegisterWebMCPTools, - useWebMCPTools, } from './use-web-mcp-tools.tsrx' export type { UsePageWebMCPToolsOptions, UseRegisterWebMCPToolsOptions, - UseWebMCPToolsOptions, } from './use-web-mcp-tools.tsrx' export type { DeepPartial, diff --git a/packages/ai-octane/src/realtime-types.ts b/packages/ai-octane/src/realtime-types.ts index 24e06016b2..8f25751e94 100644 --- a/packages/ai-octane/src/realtime-types.ts +++ b/packages/ai-octane/src/realtime-types.ts @@ -1,5 +1,6 @@ import type { AnyClientTool, + RealtimeAdapter, RealtimeMessage, RealtimeMode, RealtimeSessionConfig, @@ -7,7 +8,6 @@ import type { RealtimeToken, UsageInfo, } from '@tanstack/ai' -import type { RealtimeAdapter } from '@tanstack/ai-client' /** * Options for the useRealtimeChat hook. diff --git a/packages/ai-octane/src/types.ts b/packages/ai-octane/src/types.ts index 61cc46e294..3544a18b1b 100644 --- a/packages/ai-octane/src/types.ts +++ b/packages/ai-octane/src/types.ts @@ -204,14 +204,6 @@ interface BaseUseChatReturn< errorText?: string }) => Promise - /** - * Respond to a tool approval request - */ - addToolApprovalResponse: (response: { - id: string // approval.id, not toolCallId - approved: boolean - }) => Promise - /** * The id of the run this client has in flight — one it started or rejoined — * or `null` when there is none (including while a run sits paused on an @@ -225,8 +217,6 @@ interface BaseUseChatReturn< */ runId: string | null interrupts: BoundInterrupts - /** @deprecated Use `interrupts`. */ - pendingInterrupts: BoundInterrupts interruptErrors: ChatInterruptState['interruptErrors'] resuming: boolean resolveInterrupts: { @@ -243,11 +233,6 @@ interface BaseUseChatReturn< resume: Array, state?: ChatResumeState, ) => Promise - /** @deprecated Use bound interrupt methods or `resumeInterruptsUnsafe`. */ - resumeInterrupts: ( - resume: Array, - state?: ChatResumeState, - ) => Promise /** * Reload the last assistant message diff --git a/packages/ai-octane/src/use-chat.tsrx b/packages/ai-octane/src/use-chat.tsrx index 10ae644e9a..d5f04fb166 100644 --- a/packages/ai-octane/src/use-chat.tsrx +++ b/packages/ai-octane/src/use-chat.tsrx @@ -113,7 +113,6 @@ export function useChat< threadId: initialOptions.threadId, }), }), - ...(initialOptions.body !== undefined && { body: initialOptions.body }), ...(initialOptions.forwardedProps !== undefined && { forwardedProps: initialOptions.forwardedProps, }), @@ -247,10 +246,6 @@ export function useChat< // changing doesn't re-run the others. `updateOptions` declares strict-optional // fields and rejects explicit `undefined` under EOPT, so guard the optional // slots before passing them. - useEffect(() => { - client.updateOptions({ body: options.body }) - }, [client, options.body]) - useEffect(() => { if (options.forwardedProps !== undefined) { client.updateOptions({ forwardedProps: options.forwardedProps }) @@ -367,20 +362,6 @@ export function useChat< [client], ) - const addToolApprovalResponse = useCallback( - async (response: { id: string; approved: boolean }) => { - await client.addToolApprovalResponse(response) - }, - [client], - ) - - const resumeInterrupts = useCallback( - async (resumeItems: Array, state?: ChatResumeState) => { - return await client.resumeInterrupts(resumeItems, state ?? undefined) - }, - [client], - ) - const resolveInterrupts = useCallback( ( resolution: @@ -479,19 +460,16 @@ export function useChat< setMessages: setMessagesManually, clear, addToolResult, - addToolApprovalResponse, queue: snapshot.queue, cancelQueued, runId: snapshot.runId, interrupts: snapshot.interruptState.interrupts, - pendingInterrupts: snapshot.interruptState.pendingInterrupts, interruptErrors: snapshot.interruptState.interruptErrors, resuming: snapshot.interruptState.resuming, resolveInterrupts, cancelInterrupts, retryInterrupts, resumeInterruptsUnsafe, - resumeInterrupts, partial, final, } as unknown as UseChatReturn diff --git a/packages/ai-octane/src/use-web-mcp-tools.tsrx b/packages/ai-octane/src/use-web-mcp-tools.tsrx index 424580d049..9686b3458e 100644 --- a/packages/ai-octane/src/use-web-mcp-tools.tsrx +++ b/packages/ai-octane/src/use-web-mcp-tools.tsrx @@ -16,12 +16,6 @@ export type UseRegisterWebMCPToolsOptions< onError?: (error: unknown) => void } -/** @deprecated Use `UseRegisterWebMCPToolsOptions`. Removed in 1.0.0. */ -export type UseWebMCPToolsOptions< - TTools extends ReadonlyArray, - TContext = InferredClientContext, -> = UseRegisterWebMCPToolsOptions - type UseRegisterWebMCPToolsArguments< TTools extends ReadonlyArray, TContext, @@ -68,9 +62,6 @@ export function useRegisterWebMCPTools< }, [tools, options]) } -/** @deprecated Use `useRegisterWebMCPTools`. Removed in 1.0.0. */ -export const useWebMCPTools = useRegisterWebMCPTools - /** Options for {@link usePageWebMCPTools}. */ export type UsePageWebMCPToolsOptions = Omit< SubscribeWebMCPToolsOptions, diff --git a/packages/ai-octane/src/use-web-mcp-tools.tsrx.d.ts b/packages/ai-octane/src/use-web-mcp-tools.tsrx.d.ts index 5bc4983535..61badd230e 100644 --- a/packages/ai-octane/src/use-web-mcp-tools.tsrx.d.ts +++ b/packages/ai-octane/src/use-web-mcp-tools.tsrx.d.ts @@ -14,11 +14,6 @@ export type UseRegisterWebMCPToolsOptions< /** Receives an asynchronous registration failure. */ onError?: (error: unknown) => void } -/** @deprecated Use `UseRegisterWebMCPToolsOptions`. Removed in 1.0.0. */ -export type UseWebMCPToolsOptions< - TTools extends ReadonlyArray, - TContext = InferredClientContext, -> = UseRegisterWebMCPToolsOptions type UseRegisterWebMCPToolsArguments< TTools extends ReadonlyArray, TContext, @@ -49,8 +44,6 @@ export declare function useRegisterWebMCPTools< tools: TTools, ...[options]: UseRegisterWebMCPToolsArguments ): void -/** @deprecated Use `useRegisterWebMCPTools`. Removed in 1.0.0. */ -export declare const useWebMCPTools: typeof useRegisterWebMCPTools /** Options for {@link usePageWebMCPTools}. */ export type UsePageWebMCPToolsOptions = Omit< SubscribeWebMCPToolsOptions, diff --git a/packages/ai-octane/tests/conformance/exports.test.ts b/packages/ai-octane/tests/conformance/exports.test.ts index db8e6a92c2..1447ad9e28 100644 --- a/packages/ai-octane/tests/conformance/exports.test.ts +++ b/packages/ai-octane/tests/conformance/exports.test.ts @@ -5,7 +5,6 @@ import { useMcpAppBridge, useRegisterWebMCPTools, usePageWebMCPTools, - useWebMCPTools, useGeneration, useGenerateImage, useGenerateAudio, @@ -24,7 +23,6 @@ describe('package exports', () => { useMcpAppBridge, useRegisterWebMCPTools, usePageWebMCPTools, - useWebMCPTools, useGeneration, useGenerateImage, useGenerateAudio, diff --git a/packages/ai-octane/tests/conformance/use-chat-fetcher.test.ts b/packages/ai-octane/tests/conformance/use-chat-fetcher.test.ts index a55281f2a1..4f762eac48 100644 --- a/packages/ai-octane/tests/conformance/use-chat-fetcher.test.ts +++ b/packages/ai-octane/tests/conformance/use-chat-fetcher.test.ts @@ -132,7 +132,7 @@ describe('useChat — fetcher transport', () => { }) const { result } = renderHook(() => - useChat({ fetcher, body: { provider: 'openai' } }), + useChat({ fetcher, forwardedProps: { provider: 'openai' } }), ) await result.current.sendMessage('hello') diff --git a/packages/ai-octane/tests/conformance/use-chat.test.ts b/packages/ai-octane/tests/conformance/use-chat.test.ts index 7fbd8f2df4..352dd4c244 100644 --- a/packages/ai-octane/tests/conformance/use-chat.test.ts +++ b/packages/ai-octane/tests/conformance/use-chat.test.ts @@ -48,7 +48,6 @@ describe('useChat', () => { { source: 'hydrate' }, ) expect(Object.isFrozen(result.current.interrupts)).toBe(true) - expect(result.current.pendingInterrupts).toBe(result.current.interrupts) expect(result.current.interrupts[0]).toMatchObject({ id: 'staged-interrupt', status: 'pending', @@ -210,7 +209,7 @@ describe('useChat', () => { }, ] const persistence = { - getItem: vi.fn(() => persistedMessages), + getItem: vi.fn(() => ({ messages: persistedMessages })), setItem: vi.fn(), removeItem: vi.fn(), } @@ -268,7 +267,7 @@ describe('useChat', () => { }, ] const persistence = { - getItem: vi.fn(() => []), + getItem: vi.fn(() => ({ messages: [] })), setItem: vi.fn(), removeItem: vi.fn(), } @@ -287,7 +286,7 @@ describe('useChat', () => { }) it('should ignore async persisted messages from a previous id', async () => { - const oldHydration = createDeferred>() + const oldHydration = createDeferred<{ messages: Array }>() const oldMessages: Array = [ { id: 'old-persisted', @@ -306,7 +305,7 @@ describe('useChat', () => { ] const persistence = { getItem: vi.fn((id: string) => - id === 'old-chat' ? oldHydration.promise : newMessages, + id === 'old-chat' ? oldHydration.promise : { messages: newMessages }, ), setItem: vi.fn(), removeItem: vi.fn(), @@ -334,7 +333,7 @@ describe('useChat', () => { }) await act(async () => { - oldHydration.resolve(oldMessages) + oldHydration.resolve({ messages: oldMessages }) await oldHydration.promise }) @@ -429,7 +428,7 @@ describe('useChat', () => { expect(result.current).not.toHaveProperty('resume') expect(result.current).not.toHaveProperty('resumeState') - expect(typeof result.current.resumeInterrupts).toBe('function') + expect(typeof result.current.resumeInterruptsUnsafe).toBe('function') expect(result.current.runId).toBeNull() }) @@ -757,7 +756,7 @@ describe('useChat', () => { const { result } = renderUseChat({ connection: adapter, - body: { provider: 'openai' }, + forwardedProps: { provider: 'openai' }, }) await result.current.sendMessage('Test', { @@ -1445,16 +1444,16 @@ describe('useChat', () => { expect(renderedText).toContain('Second response') }) - it('should handle body changes', () => { + it('should handle forwardedProps changes', () => { const adapter = createMockConnectionAdapter() const { result, rerender } = renderUseChat({ connection: adapter, - body: { userId: '123' }, + forwardedProps: { userId: '123' }, }) rerender({ connection: adapter, - body: { userId: '456' }, + forwardedProps: { userId: '456' }, }) // Should not throw @@ -1913,57 +1912,6 @@ describe('useChat', () => { }) } }) - - it('should handle addToolApprovalResponse', async () => { - const toolCalls = createToolCallChunks([ - { id: 'tool-1', name: 'testTool', arguments: '{"param": "value"}' }, - ]) - const adapter = createMockConnectionAdapter({ chunks: toolCalls }) - const { result } = renderUseChat({ - connection: adapter, - }) - - await result.current.sendMessage('Test') - - await waitFor(() => { - const assistantMessage = result.current.messages.find( - (m) => m.role === 'assistant', - ) - expect(assistantMessage).toBeDefined() - }) - - // Find tool call with approval - const assistantMessage = result.current.messages.find( - (m) => m.role === 'assistant', - ) - const toolCallPart = assistantMessage?.parts.find( - (p) => p.type === 'tool-call' && p.approval, - ) - - if ( - toolCallPart && - toolCallPart.type === 'tool-call' && - toolCallPart.approval - ) { - await result.current.addToolApprovalResponse({ - id: toolCallPart.approval.id, - approved: true, - }) - - // Should update approval state - await waitFor(() => { - const updatedMessage = result.current.messages.find( - (m) => m.role === 'assistant', - ) - const updatedToolCall = updatedMessage?.parts.find( - (p) => p.type === 'tool-call' && p.id === toolCallPart.id, - ) - if (updatedToolCall && updatedToolCall.type === 'tool-call') { - expect(updatedToolCall.approval?.approved).toBe(true) - } - }) - } - }) }) }) @@ -2487,7 +2435,7 @@ describe('useChat', () => { b: [message('b')], } const persistence = { - getItem: vi.fn((id: string) => stored[id] ?? []), + getItem: vi.fn((id: string) => ({ messages: stored[id] ?? [] })), setItem: vi.fn(), removeItem: vi.fn(), } diff --git a/packages/ai-octane/tests/conformance/use-realtime-chat.test.ts b/packages/ai-octane/tests/conformance/use-realtime-chat.test.ts index 5e7b9f1ccc..2aba12e98f 100644 --- a/packages/ai-octane/tests/conformance/use-realtime-chat.test.ts +++ b/packages/ai-octane/tests/conformance/use-realtime-chat.test.ts @@ -1,12 +1,13 @@ import { act, renderHook } from '@octanejs/testing-library' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { + RealtimeAdapter, + RealtimeConnection, RealtimeEvent, RealtimeEventPayloads, RealtimeToken, UsageInfo, } from '@tanstack/ai' -import type { RealtimeAdapter, RealtimeConnection } from '@tanstack/ai-client' import { useRealtimeChat } from '../../src/use-realtime-chat.tsrx' import type { UseRealtimeChatOptions } from '../../src/realtime-types' diff --git a/packages/ai-octane/tests/conformance/use-web-mcp-tools.test.ts b/packages/ai-octane/tests/conformance/use-web-mcp-tools.test.ts index 4340f2fa1a..a859c6c64e 100644 --- a/packages/ai-octane/tests/conformance/use-web-mcp-tools.test.ts +++ b/packages/ai-octane/tests/conformance/use-web-mcp-tools.test.ts @@ -5,7 +5,6 @@ import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest' import { usePageWebMCPTools, useRegisterWebMCPTools, - useWebMCPTools, } from '../../src/use-web-mcp-tools.tsrx' import type { AnyClientTool } from '@tanstack/ai/client' import type { UseRegisterWebMCPToolsOptions } from '../../src/use-web-mcp-tools.tsrx' @@ -204,12 +203,6 @@ describe('useRegisterWebMCPTools', () => { }) }) -describe('useWebMCPTools', () => { - it('is a deprecated alias of useRegisterWebMCPTools', () => { - expect(useWebMCPTools).toBe(useRegisterWebMCPTools) - }) -}) - function installPageTools(names: Array) { const target = new EventTarget() const modelContext = { diff --git a/packages/ai-octane/typetests/use-chat.test-d.tsx b/packages/ai-octane/typetests/use-chat.test-d.tsx index 475e16c6cd..a76b1b7a07 100644 --- a/packages/ai-octane/typetests/use-chat.test-d.tsx +++ b/packages/ai-octane/typetests/use-chat.test-d.tsx @@ -88,7 +88,6 @@ describe('useChat() return type', () => { expectTypeOf().toBeFunction() expectTypeOf().toBeFunction() expectTypeOf().toBeFunction() - expectTypeOf().toBeFunction() expectTypeOf().toBeFunction() expectTypeOf().toMatchTypeOf< (content: string, options?: SendMessageOptions) => Promise diff --git a/packages/ai-ollama/src/adapters/text.ts b/packages/ai-ollama/src/adapters/text.ts index ca1b59df65..fcd9f6344a 100644 --- a/packages/ai-ollama/src/adapters/text.ts +++ b/packages/ai-ollama/src/adapters/text.ts @@ -301,19 +301,16 @@ export class OllamaTextAdapter extends BaseTextAdapter< logger: InternalLogger, ): AsyncIterable { let accumulatedContent = '' - let accumulatedReasoning = '' const toolCallsEmitted = new Set() // AG-UI lifecycle tracking const runId = options.runId ?? generateId('run') const threadId = options.threadId ?? generateId('thread') const messageId = generateId('msg') - let stepId: string | null = null let reasoningMessageId: string | null = null let hasClosedReasoning = false let hasEmittedRunStarted = false let hasEmittedTextMessageStart = false - let hasEmittedStepStarted = false for await (const chunk of stream) { logger.provider(`provider=ollama`, { chunk }) @@ -496,10 +493,8 @@ export class OllamaTextAdapter extends BaseTextAdapter< } if (chunk.message.thinking) { - // Emit STEP_STARTED and REASONING events on first thinking content - if (!hasEmittedStepStarted) { - hasEmittedStepStarted = true - stepId = generateId('step') + // Emit REASONING start events on first thinking content + if (!reasoningMessageId) { reasoningMessageId = generateId('msg') // Spec REASONING events @@ -516,41 +511,15 @@ export class OllamaTextAdapter extends BaseTextAdapter< model: chunk.model, timestamp: Date.now(), } - - // Legacy STEP events (kept during transition) - yield { - type: EventType.STEP_STARTED, - stepName: stepId, - stepId, - model: chunk.model, - timestamp: Date.now(), - stepType: 'thinking', - } } - accumulatedReasoning += chunk.message.thinking - - // Spec REASONING content event — reasoningMessageId is set in the - // hasEmittedStepStarted block above (entered on the same `thinking` path) - if (reasoningMessageId) { - yield { - type: EventType.REASONING_MESSAGE_CONTENT, - messageId: reasoningMessageId, - delta: chunk.message.thinking, - model: chunk.model, - timestamp: Date.now(), - } - } - - // Legacy STEP event + // Spec REASONING content event yield { - type: EventType.STEP_FINISHED, - stepName: stepId || generateId('step'), - stepId: stepId || generateId('step'), + type: EventType.REASONING_MESSAGE_CONTENT, + messageId: reasoningMessageId, + delta: chunk.message.thinking, model: chunk.model, timestamp: Date.now(), - delta: chunk.message.thinking, - content: accumulatedReasoning, } } } diff --git a/packages/ai-openai/src/adapters/transcription.ts b/packages/ai-openai/src/adapters/transcription.ts index be3b9f42c8..1d9ca3c176 100644 --- a/packages/ai-openai/src/adapters/transcription.ts +++ b/packages/ai-openai/src/adapters/transcription.ts @@ -54,9 +54,7 @@ function mapDiarizedSegmentId(id: string, index: number): number { */ /** * Duration-billed usage: zeroed token fields with the billed seconds carried - * on `billed` and, for backward compatibility, the deprecated - * `durationSeconds`. Shared by the gpt-4o duration branch and the whisper-1 - * path so the two fields can't drift apart. + * on `billed`. Shared by the gpt-4o duration branch and the whisper-1 path. */ function durationUsage(seconds: number): TokenUsage { return { @@ -64,7 +62,6 @@ function durationUsage(seconds: number): TokenUsage { completionTokens: 0, totalTokens: 0, billed: { quantity: seconds, unit: 'seconds' }, - durationSeconds: seconds, } } diff --git a/packages/ai-openai/src/text/text-provider-options.ts b/packages/ai-openai/src/text/text-provider-options.ts index 3e938f8ed9..f3410c430d 100644 --- a/packages/ai-openai/src/text/text-provider-options.ts +++ b/packages/ai-openai/src/text/text-provider-options.ts @@ -1,18 +1,18 @@ import type OpenAI from 'openai' import type { ResponseInput } from 'openai/resources/responses/responses' -import type { ApplyPatchTool } from '../tools/apply-patch-tool' -import type { CodeInterpreterTool } from '../tools/code-interpreter-tool' -import type { ComputerUseTool } from '../tools/computer-use-tool' -import type { CustomTool } from '../tools/custom-tool' -import type { FileSearchTool } from '../tools/file-search-tool' -import type { FunctionTool } from '../tools/function-tool' -import type { ImageGenerationTool } from '../tools/image-generation-tool' -import type { LocalShellTool } from '../tools/local-shell-tool' -import type { MCPTool } from '../tools/mcp-tool' -import type { ShellTool } from '../tools/shell-tool' +import type { ApplyPatchToolConfig } from '../tools/apply-patch-tool' +import type { CodeInterpreterToolConfig } from '../tools/code-interpreter-tool' +import type { ComputerUseToolConfig } from '../tools/computer-use-tool' +import type { CustomToolConfig } from '../tools/custom-tool' +import type { FileSearchToolConfig } from '../tools/file-search-tool' +import type { FunctionToolConfig } from '../tools/function-tool' +import type { ImageGenerationToolConfig } from '../tools/image-generation-tool' +import type { LocalShellToolConfig } from '../tools/local-shell-tool' +import type { MCPToolConfig } from '../tools/mcp-tool' +import type { ShellToolConfig } from '../tools/shell-tool' import type { ToolChoice } from '../tools/tool-choice' -import type { WebSearchPreviewTool } from '../tools/web-search-preview-tool' -import type { WebSearchTool } from '../tools/web-search-tool' +import type { WebSearchPreviewToolConfig } from '../tools/web-search-preview-tool' +import type { WebSearchToolConfig } from '../tools/web-search-tool' /** Sampling controls shared by all Responses-API models. */ export interface OpenAISamplingOptions { @@ -241,18 +241,18 @@ https://platform.openai.com/docs/api-reference/responses/create#responses_create * https://platform.openai.com/docs/guides/tools-web-search */ tools?: Array< - | FunctionTool - | FileSearchTool - | ComputerUseTool - | WebSearchTool - | MCPTool - | CodeInterpreterTool - | ImageGenerationTool - | ShellTool - | LocalShellTool - | CustomTool - | WebSearchPreviewTool - | ApplyPatchTool + | FunctionToolConfig + | FileSearchToolConfig + | ComputerUseToolConfig + | WebSearchToolConfig + | MCPToolConfig + | CodeInterpreterToolConfig + | ImageGenerationToolConfig + | ShellToolConfig + | LocalShellToolConfig + | CustomToolConfig + | WebSearchPreviewToolConfig + | ApplyPatchToolConfig > } diff --git a/packages/ai-openai/src/tools/apply-patch-tool.ts b/packages/ai-openai/src/tools/apply-patch-tool.ts index ab4ed63dfe..f38b866cae 100644 --- a/packages/ai-openai/src/tools/apply-patch-tool.ts +++ b/packages/ai-openai/src/tools/apply-patch-tool.ts @@ -3,7 +3,6 @@ import type { ProviderTool } from '@tanstack/ai' export { type ApplyPatchToolConfig, - type ApplyPatchTool, convertApplyPatchToolToAdapterFormat, } from '@tanstack/openai-base' diff --git a/packages/ai-openai/src/tools/code-interpreter-tool.ts b/packages/ai-openai/src/tools/code-interpreter-tool.ts index 52c43d89fb..629a3df806 100644 --- a/packages/ai-openai/src/tools/code-interpreter-tool.ts +++ b/packages/ai-openai/src/tools/code-interpreter-tool.ts @@ -4,7 +4,6 @@ import type { CodeInterpreterToolConfig } from '@tanstack/openai-base' export { type CodeInterpreterToolConfig, - type CodeInterpreterTool, convertCodeInterpreterToolToAdapterFormat, } from '@tanstack/openai-base' diff --git a/packages/ai-openai/src/tools/computer-use-tool.ts b/packages/ai-openai/src/tools/computer-use-tool.ts index 3f65dddb7c..3a93450035 100644 --- a/packages/ai-openai/src/tools/computer-use-tool.ts +++ b/packages/ai-openai/src/tools/computer-use-tool.ts @@ -4,7 +4,6 @@ import type { ComputerUseToolConfig } from '@tanstack/openai-base' export { type ComputerUseToolConfig, - type ComputerUseTool, convertComputerUseToolToAdapterFormat, } from '@tanstack/openai-base' diff --git a/packages/ai-openai/src/tools/custom-tool.ts b/packages/ai-openai/src/tools/custom-tool.ts index 9d898a8975..625e78e32a 100644 --- a/packages/ai-openai/src/tools/custom-tool.ts +++ b/packages/ai-openai/src/tools/custom-tool.ts @@ -1,6 +1,5 @@ export { type CustomToolConfig, - type CustomTool, convertCustomToolToAdapterFormat, customTool, } from '@tanstack/openai-base' diff --git a/packages/ai-openai/src/tools/file-search-tool.ts b/packages/ai-openai/src/tools/file-search-tool.ts index c90af10112..172bfdeb51 100644 --- a/packages/ai-openai/src/tools/file-search-tool.ts +++ b/packages/ai-openai/src/tools/file-search-tool.ts @@ -4,7 +4,6 @@ import type { FileSearchToolConfig } from '@tanstack/openai-base' export { type FileSearchToolConfig, - type FileSearchTool, convertFileSearchToolToAdapterFormat, } from '@tanstack/openai-base' diff --git a/packages/ai-openai/src/tools/function-tool.ts b/packages/ai-openai/src/tools/function-tool.ts index fefd464330..ed66b943e6 100644 --- a/packages/ai-openai/src/tools/function-tool.ts +++ b/packages/ai-openai/src/tools/function-tool.ts @@ -1,5 +1,4 @@ export { type FunctionToolConfig, - type FunctionTool, convertFunctionToolToAdapterFormat, } from '@tanstack/openai-base' diff --git a/packages/ai-openai/src/tools/image-generation-tool.ts b/packages/ai-openai/src/tools/image-generation-tool.ts index d621889f93..1dde675480 100644 --- a/packages/ai-openai/src/tools/image-generation-tool.ts +++ b/packages/ai-openai/src/tools/image-generation-tool.ts @@ -4,7 +4,6 @@ import type { ImageGenerationToolConfig } from '@tanstack/openai-base' export { type ImageGenerationToolConfig, - type ImageGenerationTool, convertImageGenerationToolToAdapterFormat, } from '@tanstack/openai-base' diff --git a/packages/ai-openai/src/tools/index.ts b/packages/ai-openai/src/tools/index.ts index 7eff9fc698..7f4e8df923 100644 --- a/packages/ai-openai/src/tools/index.ts +++ b/packages/ai-openai/src/tools/index.ts @@ -5,53 +5,45 @@ export { convertApplyPatchToolToAdapterFormat, type OpenAIApplyPatchTool, type ApplyPatchToolConfig, - type ApplyPatchTool, } from './apply-patch-tool' export { codeInterpreterTool, convertCodeInterpreterToolToAdapterFormat, type OpenAICodeInterpreterTool, type CodeInterpreterToolConfig, - type CodeInterpreterTool, } from './code-interpreter-tool' export { computerUseTool, convertComputerUseToolToAdapterFormat, type OpenAIComputerUseTool, type ComputerUseToolConfig, - type ComputerUseTool, } from './computer-use-tool' export { customTool, convertCustomToolToAdapterFormat, type CustomToolConfig, - type CustomTool, } from './custom-tool' export { fileSearchTool, convertFileSearchToolToAdapterFormat, type OpenAIFileSearchTool, type FileSearchToolConfig, - type FileSearchTool, } from './file-search-tool' export { convertFunctionToolToAdapterFormat, type FunctionToolConfig, - type FunctionTool, } from './function-tool' export { imageGenerationTool, convertImageGenerationToolToAdapterFormat, type OpenAIImageGenerationTool, type ImageGenerationToolConfig, - type ImageGenerationTool, } from './image-generation-tool' export { localShellTool, convertLocalShellToolToAdapterFormat, type OpenAILocalShellTool, type LocalShellToolConfig, - type LocalShellTool, } from './local-shell-tool' export { mcpTool, @@ -59,28 +51,24 @@ export { convertMCPToolToAdapterFormat, type OpenAIMCPTool, type MCPToolConfig, - type MCPTool, } from './mcp-tool' export { shellTool, convertShellToolToAdapterFormat, type OpenAIShellTool, type ShellToolConfig, - type ShellTool, } from './shell-tool' export { webSearchPreviewTool, convertWebSearchPreviewToolToAdapterFormat, type OpenAIWebSearchPreviewTool, type WebSearchPreviewToolConfig, - type WebSearchPreviewTool, } from './web-search-preview-tool' export { webSearchTool, convertWebSearchToolToAdapterFormat, type OpenAIWebSearchTool, type WebSearchToolConfig, - type WebSearchTool, } from './web-search-tool' export { type ToolChoice } from './tool-choice' diff --git a/packages/ai-openai/src/tools/local-shell-tool.ts b/packages/ai-openai/src/tools/local-shell-tool.ts index f49850b847..ff2fbd6e37 100644 --- a/packages/ai-openai/src/tools/local-shell-tool.ts +++ b/packages/ai-openai/src/tools/local-shell-tool.ts @@ -3,7 +3,6 @@ import type { ProviderTool } from '@tanstack/ai' export { type LocalShellToolConfig, - type LocalShellTool, convertLocalShellToolToAdapterFormat, } from '@tanstack/openai-base' diff --git a/packages/ai-openai/src/tools/mcp-tool.ts b/packages/ai-openai/src/tools/mcp-tool.ts index 73c6b95b78..3de78fb5ec 100644 --- a/packages/ai-openai/src/tools/mcp-tool.ts +++ b/packages/ai-openai/src/tools/mcp-tool.ts @@ -4,7 +4,6 @@ import type { MCPToolConfig } from '@tanstack/openai-base' export { type MCPToolConfig, - type MCPTool, validateMCPtool, convertMCPToolToAdapterFormat, } from '@tanstack/openai-base' diff --git a/packages/ai-openai/src/tools/shell-tool.ts b/packages/ai-openai/src/tools/shell-tool.ts index 77e45acfc5..8faaf16063 100644 --- a/packages/ai-openai/src/tools/shell-tool.ts +++ b/packages/ai-openai/src/tools/shell-tool.ts @@ -4,7 +4,6 @@ import type { ShellToolFactoryConfig } from '@tanstack/openai-base' export { type ShellToolConfig, - type ShellTool, type ShellToolFactoryConfig, convertShellToolToAdapterFormat, } from '@tanstack/openai-base' diff --git a/packages/ai-openai/src/tools/web-search-preview-tool.ts b/packages/ai-openai/src/tools/web-search-preview-tool.ts index b822bafbfd..40f316ee97 100644 --- a/packages/ai-openai/src/tools/web-search-preview-tool.ts +++ b/packages/ai-openai/src/tools/web-search-preview-tool.ts @@ -4,7 +4,6 @@ import type { WebSearchPreviewToolConfig } from '@tanstack/openai-base' export { type WebSearchPreviewToolConfig, - type WebSearchPreviewTool, convertWebSearchPreviewToolToAdapterFormat, } from '@tanstack/openai-base' diff --git a/packages/ai-openai/src/tools/web-search-tool.ts b/packages/ai-openai/src/tools/web-search-tool.ts index bdb39c944f..33421a6cf2 100644 --- a/packages/ai-openai/src/tools/web-search-tool.ts +++ b/packages/ai-openai/src/tools/web-search-tool.ts @@ -4,7 +4,6 @@ import type { WebSearchToolConfig } from '@tanstack/openai-base' export { type WebSearchToolConfig, - type WebSearchTool, convertWebSearchToolToAdapterFormat, } from '@tanstack/openai-base' diff --git a/packages/ai-openai/tests/transcription-adapter.test.ts b/packages/ai-openai/tests/transcription-adapter.test.ts index f04d7476ac..1ea8dae57c 100644 --- a/packages/ai-openai/tests/transcription-adapter.test.ts +++ b/packages/ai-openai/tests/transcription-adapter.test.ts @@ -630,7 +630,6 @@ describe('OpenAI transcription adapter', () => { completionTokens: 0, totalTokens: 0, billed: { quantity: 2.5, unit: 'seconds' }, - durationSeconds: 2.5, }) }) diff --git a/packages/ai-openai/tests/transcription-usage.test.ts b/packages/ai-openai/tests/transcription-usage.test.ts index e084a79d15..e67cb13161 100644 --- a/packages/ai-openai/tests/transcription-usage.test.ts +++ b/packages/ai-openai/tests/transcription-usage.test.ts @@ -98,7 +98,6 @@ describe('OpenAI transcription usage', () => { completionTokens: 0, totalTokens: 0, billed: { quantity: 12.5, unit: 'seconds' }, - durationSeconds: 12.5, }) }) }) diff --git a/packages/ai-openai/tests/video-adapter.test.ts b/packages/ai-openai/tests/video-adapter.test.ts index 2f400f0bd3..81930536c6 100644 --- a/packages/ai-openai/tests/video-adapter.test.ts +++ b/packages/ai-openai/tests/video-adapter.test.ts @@ -252,25 +252,4 @@ describe('OpenAI Video Adapter', () => { expect(result.contentType).toBe('video/mp4') await expect(new Response(result.body).text()).resolves.toBe('mp4-bytes') }) - - it('getVideoUrl buffers the download into a data URL when the job has no url', async () => { - const adapter = createOpenaiVideo('sora-2', 'test-api-key') - ;(adapter as unknown as { client: { videos: unknown } }).client = { - videos: { - retrieve: vi - .fn() - .mockResolvedValue({ id: 'job-1', status: 'completed' }), - downloadContent: vi.fn().mockResolvedValue( - new Response('mp4-bytes', { - headers: { 'content-type': 'video/mp4' }, - }), - ), - }, - } - - await expect(adapter.getVideoUrl('job-1')).resolves.toEqual({ - jobId: 'job-1', - url: `data:video/mp4;base64,${btoa('mp4-bytes')}`, - }) - }) }) diff --git a/packages/ai-openrouter/src/adapters/rerank.ts b/packages/ai-openrouter/src/adapters/rerank.ts index ea0f4d4d7c..3b4a4d3a77 100644 --- a/packages/ai-openrouter/src/adapters/rerank.ts +++ b/packages/ai-openrouter/src/adapters/rerank.ts @@ -80,7 +80,6 @@ export class OpenRouterRerankAdapter< ...(response.usage?.searchUnits !== undefined ? { billed: { quantity: response.usage.searchUnits, unit: 'units' }, - unitsBilled: response.usage.searchUnits, } : {}), ...(response.usage?.cost !== undefined diff --git a/packages/ai-openrouter/src/adapters/responses-text.ts b/packages/ai-openrouter/src/adapters/responses-text.ts index 901fcd4dc6..7df092f92a 100644 --- a/packages/ai-openrouter/src/adapters/responses-text.ts +++ b/packages/ai-openrouter/src/adapters/responses-text.ts @@ -432,7 +432,6 @@ export class OpenRouterResponsesTextAdapter< let accumulatedReasoning = '' let hasEmittedTextMessageStart = false let reasoningMessageId: string | undefined - let stepId: string | undefined let hasClosedReasoning = false let model: string = chatOptions.model let responseId: string | undefined @@ -464,18 +463,7 @@ export class OpenRouterResponsesTextAdapter< model, timestamp: Date.now(), } - if (stepId) { - yield { - type: EventType.STEP_FINISHED, - stepName: stepId, - stepId, - model, - timestamp: Date.now(), - content: accumulatedReasoning, - } - } reasoningMessageId = undefined - stepId = undefined hasClosedReasoning = false } }.bind(this) @@ -485,7 +473,6 @@ export class OpenRouterResponsesTextAdapter< }): Generator { if (reasoningMessageId) return reasoningMessageId = generateId(this.name) - stepId = generateId(this.name) yield { type: EventType.REASONING_START, messageId: reasoningMessageId, @@ -499,14 +486,6 @@ export class OpenRouterResponsesTextAdapter< model, timestamp: Date.now(), } - yield { - type: EventType.STEP_STARTED, - stepName: stepId, - stepId, - model, - timestamp: Date.now(), - stepType: 'thinking', - } }.bind(this) try { @@ -953,7 +932,6 @@ export class OpenRouterResponsesTextAdapter< this.makeStructuredOutputCompatible.bind(this), ) let accumulatedContent = '' - let accumulatedReasoning = '' let hasStreamedContentDeltas = false let hasStreamedReasoningDeltas = false @@ -962,7 +940,6 @@ export class OpenRouterResponsesTextAdapter< let responseId: string | undefined let reportedModel: string | undefined - let stepId: string | null = null let hasEmittedTextMessageStart = false let reasoningMessageId: string | undefined let hasClosedReasoning = false @@ -974,7 +951,6 @@ export class OpenRouterResponsesTextAdapter< const openReasoning = function* (): Generator { if (reasoningMessageId) return reasoningMessageId = generateId(adapterName) - stepId = generateId(adapterName) const timestamp = Date.now() const currentModel = emitModel() yield { @@ -990,14 +966,6 @@ export class OpenRouterResponsesTextAdapter< model: currentModel, timestamp, } - yield { - type: EventType.STEP_STARTED, - stepName: stepId, - stepId, - model: currentModel, - timestamp, - stepType: 'thinking', - } } const closeReasoning = function* (): Generator { @@ -1017,20 +985,8 @@ export class OpenRouterResponsesTextAdapter< model: currentModel, timestamp, } - if (stepId) { - yield { - type: EventType.STEP_FINISHED, - stepName: stepId, - stepId, - model: currentModel, - timestamp, - content: accumulatedReasoning, - } - } reasoningMessageId = undefined - stepId = null hasClosedReasoning = false - accumulatedReasoning = '' } const emitReasoningDelta = function* ( @@ -1039,7 +995,6 @@ export class OpenRouterResponsesTextAdapter< if (!text) return yield* openReasoning() if (!reasoningMessageId) return - accumulatedReasoning += text hasStreamedReasoningDeltas = true yield { type: EventType.REASONING_MESSAGE_CONTENT, @@ -1136,9 +1091,7 @@ export class OpenRouterResponsesTextAdapter< hasEmittedTextMessageStart = false reasoningMessageId = undefined hasClosedReasoning = false - stepId = null accumulatedContent = '' - accumulatedReasoning = '' } // response.failed and response.incomplete are TERMINAL events. diff --git a/packages/ai-openrouter/src/adapters/text.ts b/packages/ai-openrouter/src/adapters/text.ts index 027913b697..6ec65281f0 100644 --- a/packages/ai-openrouter/src/adapters/text.ts +++ b/packages/ai-openrouter/src/adapters/text.ts @@ -473,7 +473,6 @@ export class OpenRouterTextAdapter< let hasEmittedTextMessageStart = false let reasoningMessageId: string | undefined let hasClosedReasoning = false - let stepId: string | undefined let lastModel: string | undefined let responseId: string | undefined let finishReason: string | null | undefined @@ -496,18 +495,7 @@ export class OpenRouterTextAdapter< model: lastModel || chatOptions.model, timestamp: Date.now(), } - if (stepId) { - yield { - type: EventType.STEP_FINISHED, - stepName: stepId, - stepId, - model: lastModel || chatOptions.model, - timestamp: Date.now(), - content: accumulatedReasoning, - } - } reasoningMessageId = undefined - stepId = undefined hasClosedReasoning = false } }.bind(this) @@ -590,7 +578,6 @@ export class OpenRouterTextAdapter< if (reasoningText) { if (!reasoningMessageId) { reasoningMessageId = generateId(this.name) - stepId = generateId(this.name) yield { type: EventType.REASONING_START, messageId: reasoningMessageId, @@ -604,14 +591,6 @@ export class OpenRouterTextAdapter< model: chunk.model || chatOptions.model, timestamp: Date.now(), } - yield { - type: EventType.STEP_STARTED, - stepName: stepId, - stepId, - model: chunk.model || chatOptions.model, - timestamp: Date.now(), - stepType: 'thinking', - } } accumulatedReasoning += reasoningText yield { @@ -897,11 +876,6 @@ export class OpenRouterTextAdapter< // Reasoning lifecycle (driven by inline reasoning extraction below). let reasoningMessageId: string | undefined let hasClosedReasoning = false - // Legacy STEP_STARTED/STEP_FINISHED pair emitted alongside REASONING_* - // for back-compat with consumers (UI, devtools) that haven't migrated - // to the spec REASONING_* events yet. - let stepId: string | undefined - let accumulatedReasoning = '' // Track whether ANY tool call lifecycle was actually completed across the // entire stream. Lets us downgrade a `tool_calls` finishReason to `stop` // when the upstream signalled tool calls but never produced a complete @@ -988,7 +962,6 @@ export class OpenRouterTextAdapter< if (reasoningText) { if (!reasoningMessageId) { reasoningMessageId = generateId(this.name) - stepId = generateId(this.name) yield { type: EventType.REASONING_START, messageId: reasoningMessageId, @@ -1002,18 +975,7 @@ export class OpenRouterTextAdapter< model: chunk.model || options.model, timestamp: Date.now(), } - // Legacy STEP_STARTED (single emission, paired with the - // STEP_FINISHED below when reasoning closes). - yield { - type: EventType.STEP_STARTED, - stepName: stepId, - stepId, - model: chunk.model || options.model, - timestamp: Date.now(), - stepType: 'thinking', - } } - accumulatedReasoning += reasoningText yield { type: EventType.REASONING_MESSAGE_CONTENT, messageId: reasoningMessageId, @@ -1049,16 +1011,6 @@ export class OpenRouterTextAdapter< model: chunk.model || options.model, timestamp: Date.now(), } - if (stepId) { - yield { - type: EventType.STEP_FINISHED, - stepName: stepId, - stepId, - model: chunk.model || options.model, - timestamp: Date.now(), - content: accumulatedReasoning, - } - } } // Emit TEXT_MESSAGE_START on first text content @@ -1291,16 +1243,6 @@ export class OpenRouterTextAdapter< model: lastModel || options.model, timestamp: Date.now(), } - if (stepId) { - yield { - type: EventType.STEP_FINISHED, - stepName: stepId, - stepId, - model: lastModel || options.model, - timestamp: Date.now(), - content: accumulatedReasoning, - } - } } // An unknown finishReason must not end the run as a success. diff --git a/packages/ai-openrouter/src/index.ts b/packages/ai-openrouter/src/index.ts index c1ad22aa01..890592a3e9 100644 --- a/packages/ai-openrouter/src/index.ts +++ b/packages/ai-openrouter/src/index.ts @@ -135,7 +135,11 @@ export { export { convertToolsToProviderFormat } from './tools/tool-converter' -export type { OpenRouterTool, FunctionTool, WebSearchTool } from './tools/index' +export type { + OpenRouterTool, + FunctionTool, + WebSearchToolConfig, +} from './tools/index' // Export provider usage types export type { OpenRouterProviderUsageDetails } from './usage' diff --git a/packages/ai-openrouter/src/tools/index.ts b/packages/ai-openrouter/src/tools/index.ts index 679a2b897c..6f39c76c73 100644 --- a/packages/ai-openrouter/src/tools/index.ts +++ b/packages/ai-openrouter/src/tools/index.ts @@ -3,7 +3,6 @@ export { convertWebSearchToolToAdapterFormat, type OpenRouterWebSearchTool, type WebSearchToolConfig, - type WebSearchTool, } from './web-search-tool' export { webFetchTool, diff --git a/packages/ai-openrouter/src/tools/web-search-tool.ts b/packages/ai-openrouter/src/tools/web-search-tool.ts index c35d80d6f1..0ffd4f1604 100644 --- a/packages/ai-openrouter/src/tools/web-search-tool.ts +++ b/packages/ai-openrouter/src/tools/web-search-tool.ts @@ -18,9 +18,6 @@ export const WEB_SEARCH_TOOL_KIND = 'openrouter.web_search' */ export type WebSearchToolConfig = OpenRouterWebSearchServerTool -/** @deprecated Renamed to `WebSearchToolConfig`. Will be removed in a future release. */ -export type WebSearchTool = WebSearchToolConfig - export type OpenRouterWebSearchTool = ProviderTool<'openrouter', 'web_search'> /** A tool is a webSearchTool() output iff its metadata carries our branded kind marker. */ diff --git a/packages/ai-openrouter/tests/openrouter-adapter.test.ts b/packages/ai-openrouter/tests/openrouter-adapter.test.ts index fc70af778c..3111bea02e 100644 --- a/packages/ai-openrouter/tests/openrouter-adapter.test.ts +++ b/packages/ai-openrouter/tests/openrouter-adapter.test.ts @@ -1222,82 +1222,6 @@ describe('OpenRouter AG-UI event emission', () => { expect(runErr.error?.code).toBeUndefined() } }) - - it('emits STEP_STARTED and STEP_FINISHED for reasoning content', async () => { - const streamChunks = [ - { - id: 'chatcmpl-123', - model: 'openai/o1-preview', - choices: [ - { - delta: { - reasoningDetails: [ - { - type: 'reasoning.text', - text: 'Let me think about this...', - }, - ], - }, - finishReason: null, - }, - ], - }, - { - id: 'chatcmpl-123', - model: 'openai/o1-preview', - choices: [ - { - delta: { content: 'The answer is 42.' }, - finishReason: null, - }, - ], - }, - { - id: 'chatcmpl-123', - model: 'openai/o1-preview', - choices: [ - { - delta: {}, - finishReason: 'stop', - }, - ], - usage: { - promptTokens: 10, - completionTokens: 20, - totalTokens: 30, - }, - }, - ] - - setupMockSdkClient(streamChunks) - const adapter = createAdapter() - const chunks: Array = [] - - for await (const chunk of adapter.chatStream({ - model: 'openai/o1-preview', - messages: [{ role: 'user', content: 'What is the meaning of life?' }], - logger: testLogger, - })) { - chunks.push(chunk) - } - - // Check for STEP_STARTED event - const stepStartedChunk = chunks.find((c) => c.type === 'STEP_STARTED') - expect(stepStartedChunk).toBeDefined() - if (stepStartedChunk?.type === 'STEP_STARTED') { - expect(stepStartedChunk.stepId).toBeDefined() - expect(stepStartedChunk.stepType).toBe('thinking') - } - - // Check for STEP_FINISHED event — emitted once when reasoning closes - const stepFinishedChunks = chunks.filter((c) => c.type === 'STEP_FINISHED') - expect(stepFinishedChunks).toHaveLength(1) - const stepFinishedChunk = stepFinishedChunks[0] - if (stepFinishedChunk?.type === 'STEP_FINISHED') { - expect(stepFinishedChunk.stepId).toBeDefined() - expect(stepFinishedChunk.content).toBe('Let me think about this...') - } - }) }) describe('OpenRouter structured output', () => { @@ -2453,79 +2377,14 @@ describe('OpenRouter duplicate event prevention', () => { }) }) -describe('OpenRouter STEP event consistency', () => { +describe('OpenRouter reasoning event consistency', () => { beforeEach(() => { vi.clearAllMocks() }) - it('every STEP_FINISHED has a preceding STEP_STARTED', async () => { - const streamChunks = [ - { - id: 'chatcmpl-step', - model: 'openai/o1-preview', - choices: [ - { - delta: { - reasoningDetails: [ - { type: 'reasoning.text', text: 'Thinking...' }, - ], - }, - finishReason: null, - }, - ], - }, - { - id: 'chatcmpl-step', - model: 'openai/o1-preview', - choices: [ - { - delta: { content: 'Answer: 42' }, - finishReason: null, - }, - ], - }, - { - id: 'chatcmpl-step', - model: 'openai/o1-preview', - choices: [{ delta: {}, finishReason: 'stop' }], - usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 }, - }, - ] - - setupMockSdkClient(streamChunks) - const adapter = createAdapter() - const chunks: Array = [] - - for await (const chunk of adapter.chatStream({ - model: 'openai/o1-preview', - messages: [{ role: 'user', content: 'What is the meaning of life?' }], - logger: testLogger, - })) { - chunks.push(chunk) - } - - const eventTypes = chunks.map((c) => c.type) - const stepFinishedIndices = eventTypes - .map((t, i) => (t === 'STEP_FINISHED' ? i : -1)) - .filter((i) => i !== -1) - const stepStartedIndices = eventTypes - .map((t, i) => (t === 'STEP_STARTED' ? i : -1)) - .filter((i) => i !== -1) - - // Every STEP_FINISHED must have a STEP_STARTED before it - expect(stepStartedIndices.length).toBeGreaterThan(0) - for (const finIdx of stepFinishedIndices) { - const hasMatchingStart = stepStartedIndices.some( - (startIdx) => startIdx < finIdx, - ) - expect(hasMatchingStart).toBe(true) - } - }) - - it('emits exactly one STEP_STARTED and one STEP_FINISHED for multi-delta reasoning', async () => { + it('emits exactly one REASONING_START and one REASONING_END for multi-delta reasoning', async () => { // When multiple reasoning deltas arrive, the adapter should emit a - // single STEP_STARTED/STEP_FINISHED pair — not one STEP_FINISHED per - // delta. A 1:N ratio causes verifiers to report orphan STEP_FINISHED. + // single REASONING_START/REASONING_END pair — not one pair per delta. const streamChunks = [ { id: 'chatcmpl-multi', @@ -2595,19 +2454,16 @@ describe('OpenRouter STEP event consistency', () => { chunks.push(chunk) } - const stepStarted = chunks.filter((c) => c.type === 'STEP_STARTED') - const stepFinished = chunks.filter((c) => c.type === 'STEP_FINISHED') + const reasoningStart = chunks.filter((c) => c.type === 'REASONING_START') + const reasoningEnd = chunks.filter((c) => c.type === 'REASONING_END') - expect(stepStarted).toHaveLength(1) - expect(stepFinished).toHaveLength(1) + expect(reasoningStart).toHaveLength(1) + expect(reasoningEnd).toHaveLength(1) }) - it('emits the spec REASONING_* lifecycle alongside the legacy STEP_* events', async () => { - // The base now exposes both the legacy STEP_STARTED/STEP_FINISHED pair - // (kept for backwards compatibility with consumers built against the - // pre-spec stream) AND the spec REASONING_START / REASONING_MESSAGE_* / - // REASONING_END events. Dropping any of the REASONING_* events would - // silently break consumers that migrated to the new shape. + it('emits the spec REASONING_* lifecycle', async () => { + // REASONING_START / REASONING_MESSAGE_* / REASONING_END are the only + // reasoning events. Dropping any of them would silently break consumers. const streamChunks = [ { id: 'r-1', @@ -2674,8 +2530,7 @@ describe('OpenRouter STEP event consistency', () => { expect(reasoningEnd).toBeGreaterThan(reasoningMessageEnd) // Joining REASONING_MESSAGE_CONTENT deltas reproduces the full reasoning - // text — the migration leaves the new-spec event shape semantically - // equivalent to the legacy STEP_FINISHED accumulator without losing data. + // text. const reasoningDeltas = chunks .filter( ( diff --git a/packages/ai-openrouter/tests/rerank-adapter.test.ts b/packages/ai-openrouter/tests/rerank-adapter.test.ts index 11a3e7ee53..cd6b638454 100644 --- a/packages/ai-openrouter/tests/rerank-adapter.test.ts +++ b/packages/ai-openrouter/tests/rerank-adapter.test.ts @@ -88,7 +88,6 @@ describe('OpenRouterRerankAdapter', () => { const result = await rerank({ adapter: adapter(), query: 'q', documents }) expect(result.usage.billed).toEqual({ quantity: 1, unit: 'units' }) - expect(result.usage.unitsBilled).toBe(1) expect(result.usage.cost).toBe(0.002) expect(result.usage.totalTokens).toBe(20) }) diff --git a/packages/ai-openrouter/tests/video-adapter.test.ts b/packages/ai-openrouter/tests/video-adapter.test.ts index a821e1ff79..a788a00385 100644 --- a/packages/ai-openrouter/tests/video-adapter.test.ts +++ b/packages/ai-openrouter/tests/video-adapter.test.ts @@ -394,25 +394,6 @@ describe('OpenRouter Video Adapter', () => { expect(result.usage).toMatchObject({ cost: 0.45 }) }) - it('getVideoUrl buffers the content into a data URL and keeps usage', async () => { - mockGetGeneration = vi.fn().mockResolvedValueOnce( - createMockJobResponse({ - status: 'completed', - unsignedUrls: [CONTENT_URL], - usage: { cost: 0.45 }, - }), - ) - mockGetVideoContent = vi - .fn() - .mockResolvedValueOnce(streamOf(new TextEncoder().encode('mp4-bytes'))) - - const result = await createAdapter().getVideoUrl('job-123') - - expect(result.url).toBe(`data:video/mp4;base64,${btoa('mp4-bytes')}`) - expect(result.usage).toMatchObject({ cost: 0.45 }) - expect(result).not.toHaveProperty('body') - }) - it('omits usage when the job reports no cost', async () => { mockGetGeneration = vi.fn().mockResolvedValueOnce( createMockJobResponse({ diff --git a/packages/ai-persistence/src/index.ts b/packages/ai-persistence/src/index.ts index 4440d726c3..5d25d0f7a4 100644 --- a/packages/ai-persistence/src/index.ts +++ b/packages/ai-persistence/src/index.ts @@ -96,11 +96,7 @@ export type { } from './reconstruct-generation' // Server helpers: retrieve a persisted generation artifact + its bytes -export { - retrieveArtifact, - retrieveBlob, - resolveArtifactBlobKey, -} from './retrieve' +export { retrieveArtifact, retrieveBlob } from './retrieve' // Byte-range helpers: parse a `Range` header (routes), clamp a range to an // object's real size (store authors) diff --git a/packages/ai-persistence/src/middleware.ts b/packages/ai-persistence/src/middleware.ts index d71982be7b..3c6a0cc26e 100644 --- a/packages/ai-persistence/src/middleware.ts +++ b/packages/ai-persistence/src/middleware.ts @@ -66,7 +66,6 @@ import type { RunAgentResumeItem, StreamChunk, Tool, - ToolApprovalResolution, BilledUsage, TokenUsage, } from '@tanstack/ai' @@ -385,54 +384,6 @@ function runMessages( const validResumeStatuses = new Set(['resolved', 'cancelled']) -function mergeMaps( - left?: ReadonlyMap, - right?: ReadonlyMap, -): Map | undefined { - if (!left && !right) return undefined - return new Map([...(left ?? []), ...(right ?? [])]) -} - -function mergeSets( - left?: ReadonlySet, - right?: ReadonlySet, -): Set | undefined { - if (!left && !right) return undefined - return new Set([...(left ?? []), ...(right ?? [])]) -} - -function mergeResumeToolState( - left: ChatResumeToolState | undefined, - right: ChatResumeToolState | undefined, -): ChatResumeToolState | undefined { - if (!left) return right - if (!right) return left - return { - approvals: mergeMaps(left.approvals, right.approvals), - clientToolResults: mergeMaps( - left.clientToolResults, - right.clientToolResults, - ), - clientToolErrors: mergeMaps(left.clientToolErrors, right.clientToolErrors), - genericInterrupts: mergeMaps( - left.genericInterrupts, - right.genericInterrupts, - ), - genericInterruptRequests: mergeMaps( - left.genericInterruptRequests, - right.genericInterruptRequests, - ), - deniedToolResults: mergeMaps( - left.deniedToolResults, - right.deniedToolResults, - ), - cancelledToolCallIds: mergeSets( - left.cancelledToolCallIds, - right.cancelledToolCallIds, - ), - } -} - function rejectMixedRunPending( pending: Array, ctx: Pick, @@ -644,11 +595,6 @@ function stringField( return typeof value[key] === 'string' ? value[key] : undefined } -function interruptKind(interrupt: InterruptRecord): string | undefined { - const metadata = objectValue(interrupt.payload.metadata) - return metadata ? stringField(metadata, 'kind') : undefined -} - function hasReservedInterruptBinding(payload: unknown): boolean { const descriptor = objectValue(payload) const metadata = objectValue(descriptor?.metadata) @@ -671,17 +617,13 @@ function isPersistedInterruptDescriptor( * Does this pending record belong to the TanStack chat resume protocol? * * An external system can persist an AG-UI descriptor in the same durable - * thread. A descriptor without a TanStack binding or legacy tool marker stays - * pending for its owner, but it does not make this resume incomplete. Older - * opaque records remain owned because their provenance cannot be known. + * thread. A descriptor without a TanStack binding stays pending for its owner, + * but it does not make this resume incomplete. Opaque records remain owned + * because their provenance cannot be known. */ function isChatOwnedPendingInterrupt(interrupt: InterruptRecord): boolean { - const kind = interruptKind(interrupt) return ( !isPersistedInterruptDescriptor(interrupt.payload) || - stringField(interrupt.payload, 'toolCallId') !== undefined || - kind === 'approval' || - kind === 'client_tool' || hasReservedInterruptBinding(interrupt.payload) ) } @@ -910,67 +852,6 @@ async function durableGenericResumeState( } } -function resolvedApprovalDecision(entry: RunAgentResumeItem): boolean { - if (entry.status === 'cancelled') return false - const payload = objectValue(entry.payload) - // Fail closed: persisted resume payloads may be malformed or truncated, so a - // missing/non-boolean `approved` denies the tool rather than running it. - return typeof payload?.approved === 'boolean' ? payload.approved : false -} - -/** - * Translate the persisted pending interrupts + the resume batch into the - * `ChatResumeToolState` the chat engine consumes. This is the server-authoritative - * counterpart to the engine's ephemeral (client-history) reconstruction: because - * the persistence flow sends empty client messages, the engine has no history to - * rebuild from, so persistence supplies the resume state directly (and clears - * `config.resume` so the ephemeral path is skipped — see `onConfig`). - */ -function resumeToolStateFromPending( - pending: Array, - resumeByInterruptId: Map, -): ChatResumeToolState | undefined { - const approvals = new Map() - const clientToolResults = new Map() - const cancelledToolCallIds = new Set() - - for (const interrupt of pending) { - const entry = resumeByInterruptId.get(interrupt.interruptId) - if (!entry) continue - if (hasReservedInterruptBinding(interrupt.payload)) continue - - const kind = interruptKind(interrupt) - const reason = stringField(interrupt.payload, 'reason') - const toolCallId = stringField(interrupt.payload, 'toolCallId') - - if (entry.status === 'cancelled' && toolCallId) { - cancelledToolCallIds.add(toolCallId) - } - - if (kind === 'approval' || reason === 'approval_required') { - approvals.set(interrupt.interruptId, resolvedApprovalDecision(entry)) - continue - } - - if ( - entry.status === 'resolved' && - toolCallId && - (kind === 'client_tool' || reason === 'client_tool_input') - ) { - clientToolResults.set(toolCallId, entry.payload) - } - } - - if ( - approvals.size === 0 && - clientToolResults.size === 0 && - cancelledToolCallIds.size === 0 - ) { - return undefined - } - return { approvals, clientToolResults, cancelledToolCallIds } -} - function interruptPayload(interrupt: unknown): Record { return interrupt && typeof interrupt === 'object' ? { ...(interrupt as Record) } @@ -1878,11 +1759,6 @@ function accumulateTokenUsage( // Provider-specific details are opaque, so retain the latest reported bag. const providerUsageDetails = next.providerUsageDetails ?? current.providerUsageDetails - const durationSeconds = sumOptionalNumber( - current.durationSeconds, - next.durationSeconds, - ) - const unitsBilled = sumOptionalNumber(current.unitsBilled, next.unitsBilled) const billed = accumulateBilled(current.billed, next.billed) const cost = sumOptionalNumber(current.cost, next.cost) @@ -1894,8 +1770,6 @@ function accumulateTokenUsage( totalTokens: current.totalTokens + next.totalTokens, ...(promptTokensDetails ? { promptTokensDetails } : {}), ...(completionTokensDetails ? { completionTokensDetails } : {}), - ...(durationSeconds !== undefined ? { durationSeconds } : {}), - ...(unitsBilled !== undefined ? { unitsBilled } : {}), ...(billed !== undefined ? { billed } : {}), ...(cost !== undefined ? { cost } : {}), ...(costDetails ? { costDetails } : {}), @@ -2256,23 +2130,14 @@ export function withPersistence( // (which needs a parentRunId and the client message history the // persistence flow deliberately omits). if ((config.resume?.length ?? 0) > 0) { - const resumeToolState = resumeToolStateFromPending( - ownedPending, - resumeByInterruptId, - ) - const genericResumeState = await durableGenericResumeState( + const resumeToolState = await durableGenericResumeState( ctx, ownedPending, config.resume ?? [], config.tools, ) patch.resume = [] - if (resumeToolState || genericResumeState) { - patch.resumeToolState = mergeResumeToolState( - resumeToolState, - genericResumeState, - ) - } + if (resumeToolState) patch.resumeToolState = resumeToolState } // Defer marking these interrupts resolved/cancelled until the run // succeeds (see commitPendingResumes). Committing here would consume the diff --git a/packages/ai-persistence/src/retrieve.ts b/packages/ai-persistence/src/retrieve.ts index 1269e49339..ecd3d4251a 100644 --- a/packages/ai-persistence/src/retrieve.ts +++ b/packages/ai-persistence/src/retrieve.ts @@ -9,9 +9,9 @@ import type { * The DEFAULT blob-store key a generation artifact's bytes are stored under, * used when `withGenerationPersistence` is given no `storageKey` mapper. * - * Reads go through {@link resolveArtifactBlobKey} instead: a record written with - * a custom `storageKey` carries its real key in `blobKey`, and recomputing the - * default would look in the wrong place. + * Reads use the record's `blobKey` instead: a record written with a custom + * `storageKey` carries its real key there, and recomputing the default would + * look in the wrong place. * * @internal */ @@ -21,18 +21,6 @@ export function artifactBlobKey( return `artifacts/${ref.runId}/${ref.artifactId}` } -/** - * The blob-store key to read an artifact's bytes from: the key recorded when it - * was written, falling back to the default convention for records written - * before `blobKey` existed. - * - * The fallback is what makes `blobKey` a non-breaking addition — and why the - * default convention can never be changed retroactively without one. - */ -export function resolveArtifactBlobKey(record: ArtifactRecord): string { - return record.blobKey ?? artifactBlobKey(record) -} - /** * Look up a persisted generation artifact's metadata by id. Returns `null` when * the persistence has no `artifacts` store or no record matches — so a serve @@ -69,9 +57,6 @@ export async function retrieveBlob( : artifact if (!record) return null - const blob = await persistence.stores.blobs?.get( - resolveArtifactBlobKey(record), - options, - ) + const blob = await persistence.stores.blobs?.get(record.blobKey, options) return blob ?? null } diff --git a/packages/ai-persistence/src/testkit/conformance.ts b/packages/ai-persistence/src/testkit/conformance.ts index 7b958783d7..770f08b861 100644 --- a/packages/ai-persistence/src/testkit/conformance.ts +++ b/packages/ai-persistence/src/testkit/conformance.ts @@ -1161,6 +1161,7 @@ export function runPersistenceConformance( ): ArtifactRecord => ({ runId: 'run-art', threadId: 'thread-art', + blobKey: 'artifacts/run-art/default', name: 'image.png', mimeType: 'image/png', size: 3, @@ -1218,7 +1219,7 @@ export function runPersistenceConformance( ) const updated = await store.get('\u{10000}') expect(updated).toMatchObject({ name: 'renamed.png', size: 9 }) - expect(updated?.blobKey).toBeUndefined() + expect(updated?.blobKey).toBe('artifacts/run-art/default') expect((await store.list('run-art')).map((r) => r.artifactId)).toEqual([ '\u{e000}', '\u{10000}', diff --git a/packages/ai-persistence/src/types.ts b/packages/ai-persistence/src/types.ts index 18eae8b56f..32e8761263 100644 --- a/packages/ai-persistence/src/types.ts +++ b/packages/ai-persistence/src/types.ts @@ -434,15 +434,11 @@ export interface ArtifactRecord { runId: string threadId: string /** - * The blob-store key these bytes actually live under. - * - * Optional for backwards compatibility: records written before this existed - * resolve via the default `artifacts//` convention. New - * records always carry it, which is what lets `storageKey` put bytes anywhere - * — a reader can no longer recompute the path, so it has to be remembered. - * Use `resolveArtifactBlobKey(record)` rather than reading it directly. + * The blob-store key these bytes actually live under. `storageKey` can put + * bytes anywhere, so a reader cannot recompute the path; it has to be + * remembered. */ - blobKey?: string + blobKey: string name: string mimeType: string size: number diff --git a/packages/ai-persistence/tests/artifact-thread.test.ts b/packages/ai-persistence/tests/artifact-thread.test.ts index 6ebd5af1f1..c91a7fdf65 100644 --- a/packages/ai-persistence/tests/artifact-thread.test.ts +++ b/packages/ai-persistence/tests/artifact-thread.test.ts @@ -7,6 +7,7 @@ const artifact = ( ): ArtifactRecord => ({ runId: 'run-1', threadId: 'thread-1', + blobKey: 'artifacts/run-1/file', name: 'file.txt', mimeType: 'text/plain', size: 1, diff --git a/packages/ai-persistence/tests/generation-artifacts.test.ts b/packages/ai-persistence/tests/generation-artifacts.test.ts index be20aa523c..9947c4c21a 100644 --- a/packages/ai-persistence/tests/generation-artifacts.test.ts +++ b/packages/ai-persistence/tests/generation-artifacts.test.ts @@ -141,7 +141,7 @@ function videoAdapter(url: string, expiresAt?: Date): VideoAdapter { jobId: 'video-job-1', status: 'completed' as const, })), - getVideoUrl: vi.fn(async () => ({ jobId: 'video-job-1', url, expiresAt })), + getVideo: vi.fn(async () => ({ jobId: 'video-job-1', url, expiresAt })), availableDurations: () => ({ kind: 'none' }), snapDuration: () => undefined, } @@ -304,30 +304,6 @@ describe('withGenerationPersistence generation artifacts', () => { ).resolves.toBe('output-image') }) - it('resolves a record written before blobKey existed via the default convention', async () => { - const persistence = memoryPersistence() - - await generateImage({ - adapter: imageAdapter(), - prompt: 'make an image', - threadId: 'thread-legacy', - runId: 'run-legacy', - middleware: [ - withGenerationPersistence(persistence, { threadId: 'thread-legacy' }), - ], - }) - const stored = (await persistence.stores.artifacts!.list('run-legacy'))[0]! - - // Simulate a row saved before the column existed: no blobKey at all. - const { blobKey: _dropped, ...legacy } = stored - await persistence.stores.artifacts!.save(legacy) - - // Still readable — the fallback is what makes blobKey a non-breaking add. - await expect( - (await retrieveBlob(persistence, legacy.artifactId))?.text(), - ).resolves.toBe('output-image') - }) - it('persists non-image media outputs', async () => { const persistence = memoryPersistence() diff --git a/packages/ai-persistence/tests/interrupts.test.ts b/packages/ai-persistence/tests/interrupts.test.ts index 810449e434..b95ffe7925 100644 --- a/packages/ai-persistence/tests/interrupts.test.ts +++ b/packages/ai-persistence/tests/interrupts.test.ts @@ -8,7 +8,6 @@ import { import type { AdapterYieldChunk, AnyTextAdapter, - ChatResumeToolState, StreamChunk, TokenUsage, Tool, @@ -136,7 +135,6 @@ const interruptFinished = ( { id: 'interrupt-1', reason: 'tool_call', - message: 'Approve the tool call?', toolCallId: 'tool-call-1', }, ], @@ -1026,199 +1024,6 @@ describe('interrupt persistence', () => { ).toBe('resolved') }) - it('fails closed: an approval resume without an `approved` flag denies the tool', async () => { - const persistence = memoryPersistence() - await persistence.stores.interrupts!.create({ - interruptId: 'approval-1', - runId: 'r1', - threadId: 't1', - requestedAt: 1, - payload: { toolCallId: 'tc1', metadata: { kind: 'approval' } }, - }) - - const run = mockAdapter([[runStarted(), text('ok'), runFinished('r1')]]) - await collect( - chat({ - adapter: run.adapter, - messages: [], - runId: 'r1', - threadId: 't1', - // Malformed/truncated persisted payload: no `approved` field. - resume: [ - { interruptId: 'approval-1', status: 'resolved', payload: {} }, - ], - middleware: [withPersistence(persistence)], - }) as AsyncIterable, - ) - - const approvals = ( - run.calls[0] as { approvals?: ReadonlyMap } - ).approvals - expect(approvals?.get('approval-1')).toBe(false) - }) - - it('honors an explicit approved:true resume payload', async () => { - const persistence = memoryPersistence() - await persistence.stores.interrupts!.create({ - interruptId: 'approval-1', - runId: 'r1', - threadId: 't1', - requestedAt: 1, - payload: { toolCallId: 'tc1', metadata: { kind: 'approval' } }, - }) - - const run = mockAdapter([[runStarted(), text('ok'), runFinished('r1')]]) - await collect( - chat({ - adapter: run.adapter, - messages: [], - runId: 'r1', - threadId: 't1', - resume: [ - { - interruptId: 'approval-1', - status: 'resolved', - payload: { approved: true }, - }, - ], - middleware: [withPersistence(persistence)], - }) as AsyncIterable, - ) - - const approvals = ( - run.calls[0] as { approvals?: ReadonlyMap } - ).approvals - expect(approvals?.get('approval-1')).toBe(true) - }) - - it('denies the tool when an approval is cancelled', async () => { - const persistence = memoryPersistence() - await persistence.stores.interrupts!.create({ - interruptId: 'approval-1', - runId: 'r1', - threadId: 't1', - requestedAt: 1, - payload: { toolCallId: 'tc1', metadata: { kind: 'approval' } }, - }) - - const run = mockAdapter([[runStarted(), text('ok'), runFinished('r1')]]) - const resumeStates: Array | undefined> = [] - const observeResumeState = defineChatMiddleware({ - name: 'observe-resume-state', - onConfig(_ctx, config) { - resumeStates.push(config.resumeToolState?.cancelledToolCallIds) - }, - }) - await collect( - chat({ - adapter: run.adapter, - messages: [], - runId: 'r1', - threadId: 't1', - resume: [{ interruptId: 'approval-1', status: 'cancelled' }], - middleware: [withPersistence(persistence), observeResumeState], - }) as AsyncIterable, - ) - - const approvals = ( - run.calls[0] as { approvals?: ReadonlyMap } - ).approvals - expect(approvals?.get('approval-1')).toBe(false) - expect(resumeStates[0]?.has('tc1')).toBe(true) - expect( - (await persistence.stores.interrupts!.get('approval-1'))?.status, - ).toBe('cancelled') - }) - - it('drops the result of a cancelled client-tool interrupt', async () => { - const persistence = memoryPersistence() - await persistence.stores.interrupts!.create({ - interruptId: 'client-1', - runId: 'r1', - threadId: 't1', - requestedAt: 1, - payload: { toolCallId: 'tc1', metadata: { kind: 'client_tool' } }, - }) - - const run = mockAdapter([[runStarted(), text('ok'), runFinished('r1')]]) - const resumeStates: Array | undefined> = [] - const observeResumeState = defineChatMiddleware({ - name: 'observe-resume-state', - onConfig(_ctx, config) { - resumeStates.push(config.resumeToolState?.cancelledToolCallIds) - }, - }) - await collect( - chat({ - adapter: run.adapter, - messages: [], - runId: 'r1', - threadId: 't1', - resume: [ - { - interruptId: 'client-1', - status: 'cancelled', - payload: { answer: 99 }, - }, - ], - middleware: [withPersistence(persistence), observeResumeState], - }) as AsyncIterable, - ) - - // A cancelled client tool never surfaces its payload as a tool result: the - // resume state carries no entry for the tool call. - const clientToolResults = ( - run.calls[0] as { clientToolResults?: ReadonlyMap } - ).clientToolResults - expect(clientToolResults?.get('tc1')).toBeUndefined() - expect(resumeStates[0]?.has('tc1')).toBe(true) - expect((await persistence.stores.interrupts!.get('client-1'))?.status).toBe( - 'cancelled', - ) - }) - - it('translates raw legacy client-tool resume payloads', async () => { - const persistence = memoryPersistence() - await persistence.stores.interrupts!.create({ - interruptId: 'client-1', - runId: 'r1', - threadId: 't1', - requestedAt: 1, - payload: { toolCallId: 'tc1', metadata: { kind: 'client_tool' } }, - }) - - const resumeStates: Array = [] - await collect( - chat({ - adapter: mockAdapter([[runStarted(), text('ok'), runFinished('r1')]]) - .adapter, - messages: [], - runId: 'r1', - threadId: 't1', - resume: [ - { - interruptId: 'client-1', - status: 'resolved', - payload: { answer: 42 }, - }, - ], - middleware: [ - withPersistence(persistence), - defineChatMiddleware({ - name: 'observe-resume-state', - onConfig(_ctx, config) { - resumeStates.push(config.resumeToolState) - }, - }), - ], - }) as AsyncIterable, - ) - - expect(resumeStates[0]?.clientToolResults?.get('tc1')).toEqual({ - answer: 42, - }) - }) - it('tolerates malformed persisted interrupt payloads without crashing', async () => { const persistence = memoryPersistence() // Payload with the wrong shapes for the defensive parsers: metadata is a @@ -1722,91 +1527,6 @@ describe('interrupt persistence', () => { ) }) - it('keeps an opaque approval when a generic interrupt is also resumed', async () => { - const persistence = memoryPersistence() - const review = defineInterrupt({ - id: 'merge-review', - responseSchema: coercedCountSchema, - }) - const first = await collect( - chat({ - adapter: mockAdapter([[runStarted(), runFinished('r1')]]).adapter, - interrupts: [review], - messages: [{ role: 'user', content: 'hi' }], - runId: 'r1', - threadId: 't1', - middleware: [ - defineChatMiddleware({ - onInterruptBoundary(ctx) { - if (ctx.phase !== 'afterModel') return - return { - interrupts: [ - review.interrupt({ - key: 'review', - reason: 'review', - message: 'Review this plan', - }), - ], - } - }, - }), - withPersistence(persistence), - ], - }) as AsyncIterable, - ) - const terminal = first.find(isInterruptTerminal) - const genericId = terminal?.outcome.interrupts[0]?.id - if (!genericId) throw new Error('Expected a persisted generic interrupt') - - await persistence.stores.interrupts!.create({ - interruptId: 'approval-1', - runId: 'r1', - threadId: 't1', - requestedAt: 2, - payload: { toolCallId: 'tc1', metadata: { kind: 'approval' } }, - }) - - const resumeStates: Array = [] - await collect( - chat({ - adapter: mockAdapter([ - [runStarted(), text('continued'), runFinished('r1')], - ]).adapter, - interrupts: [review], - messages: [], - runId: 'r1', - threadId: 't1', - resume: [ - { - interruptId: 'approval-1', - status: 'resolved', - payload: { approved: true }, - }, - { - interruptId: genericId, - status: 'resolved', - payload: { count: '2' }, - }, - ], - middleware: [ - withPersistence(persistence), - defineChatMiddleware({ - name: 'observe-merged-resume-state', - onConfig(_ctx, config) { - resumeStates.push(config.resumeToolState) - }, - onInterruptResolution() { - return { toolResume: 'continue' } - }, - }), - ], - }) as AsyncIterable, - ) - - expect(resumeStates[0]?.approvals?.get('approval-1')).toBe(true) - expect(resumeStates[0]?.genericInterruptRequests?.has(genericId)).toBe(true) - }) - it('commits a mixed resume batch once when the store supports commitBatch', async () => { const persistence = memoryPersistence() const store = persistence.stores.interrupts! diff --git a/packages/ai-persistence/tests/with-persistence.test.ts b/packages/ai-persistence/tests/with-persistence.test.ts index 44921f6e54..83fa4a41cc 100644 --- a/packages/ai-persistence/tests/with-persistence.test.ts +++ b/packages/ai-persistence/tests/with-persistence.test.ts @@ -319,7 +319,6 @@ describe('withPersistence (state-only)', () => { totalTokens: 12, promptTokensDetails: { cachedTokens: 3 }, billed: { quantity: 2, unit: 'units' }, - unitsBilled: 2, cost: 1, }, }, @@ -340,7 +339,6 @@ describe('withPersistence (state-only)', () => { completionTokensDetails: { reasoningTokens: 2 }, providerUsageDetails: { requestId: 'final' }, billed: { quantity: 1, unit: 'units' }, - unitsBilled: 1, cost: 2, }), ], @@ -365,7 +363,6 @@ describe('withPersistence (state-only)', () => { completionTokensDetails: { reasoningTokens: 2 }, providerUsageDetails: { requestId: 'final' }, billed: { quantity: 3, unit: 'units' }, - unitsBilled: 3, cost: 3, }) }) @@ -1104,8 +1101,9 @@ describe('withPersistence (state-only)', () => { timestamp: 1, }, { - type: EventType.STEP_STARTED, - stepName: 'think-1', + type: EventType.REASONING_MESSAGE_START, + messageId: 'reasoning-1', + role: 'reasoning', timestamp: 1, }, { @@ -1115,8 +1113,8 @@ describe('withPersistence (state-only)', () => { timestamp: 1, }, { - type: EventType.STEP_FINISHED, - stepName: 'think-1', + type: EventType.REASONING_MESSAGE_END, + messageId: 'reasoning-1', timestamp: 1, }, ev.text('Ada'), diff --git a/packages/ai-preact/src/chat-ui/create-chat-hook.tsx b/packages/ai-preact/src/chat-ui/create-chat-hook.tsx index 0f4687f901..af9b4b9981 100644 --- a/packages/ai-preact/src/chat-ui/create-chat-hook.tsx +++ b/packages/ai-preact/src/chat-ui/create-chat-hook.tsx @@ -17,7 +17,6 @@ type ChatInstanceOverrides = { threadId?: string live?: boolean forwardedProps?: Record - body?: Record initialMessages?: HeadlessOptions['initialMessages'] } diff --git a/packages/ai-preact/src/create-chat-hook.ts b/packages/ai-preact/src/create-chat-hook.ts index cb68da7fc9..2c2ba24d27 100644 --- a/packages/ai-preact/src/create-chat-hook.ts +++ b/packages/ai-preact/src/create-chat-hook.ts @@ -11,7 +11,6 @@ type ChatHookOverrides< threadId?: string live?: boolean forwardedProps?: Record - body?: Record initialMessages?: UseChatOptions< TTools, TContext, diff --git a/packages/ai-preact/src/index.ts b/packages/ai-preact/src/index.ts index f923671be2..974a9e6c98 100644 --- a/packages/ai-preact/src/index.ts +++ b/packages/ai-preact/src/index.ts @@ -3,15 +3,10 @@ export { createChatHook } from './create-chat-hook' export { useByok } from './use-byok' export { useMcpAppBridge } from './use-mcp-app-bridge' export type { UseMcpAppBridgeOptions } from './use-mcp-app-bridge' -export { - usePageWebMCPTools, - useRegisterWebMCPTools, - useWebMCPTools, -} from './use-web-mcp-tools' +export { usePageWebMCPTools, useRegisterWebMCPTools } from './use-web-mcp-tools' export type { UsePageWebMCPToolsOptions, UseRegisterWebMCPToolsOptions, - UseWebMCPToolsOptions, } from './use-web-mcp-tools' export type { UseChatOptions, diff --git a/packages/ai-preact/src/types.ts b/packages/ai-preact/src/types.ts index 6af42c8597..5c6298a1c6 100644 --- a/packages/ai-preact/src/types.ts +++ b/packages/ai-preact/src/types.ts @@ -54,7 +54,7 @@ export type { * All other callbacks (onResponse, onChunk, onFinish, onError) are * passed through to the underlying ChatClient and can be used for side effects. * - * Note: Connection and body changes will recreate the ChatClient instance. + * Note: Connection changes will recreate the ChatClient instance. * To update these options, remount the component or use a key prop. */ export type UseChatOptions< @@ -137,14 +137,6 @@ export interface UseChatReturn< errorText?: string }) => Promise - /** - * Respond to a tool approval request - */ - addToolApprovalResponse: (response: { - id: string // approval.id, not toolCallId - approved: boolean - }) => Promise - /** * The id of the run this client has in flight — one it started or rejoined — * or `null` when there is none (including while a run sits paused on an @@ -158,8 +150,6 @@ export interface UseChatReturn< */ runId: string | null interrupts: BoundInterrupts - /** @deprecated Use `interrupts`. */ - pendingInterrupts: BoundInterrupts interruptErrors: ChatInterruptState['interruptErrors'] resuming: boolean resolveInterrupts: { @@ -176,11 +166,6 @@ export interface UseChatReturn< resume: Array, state?: ChatResumeState, ) => Promise - /** @deprecated Use bound interrupt methods or `resumeInterruptsUnsafe`. */ - resumeInterrupts: ( - resume: Array, - state?: ChatResumeState, - ) => Promise /** * Reload the last assistant message diff --git a/packages/ai-preact/src/use-chat.ts b/packages/ai-preact/src/use-chat.ts index 309f4fb478..bd8a2eb82c 100644 --- a/packages/ai-preact/src/use-chat.ts +++ b/packages/ai-preact/src/use-chat.ts @@ -94,7 +94,6 @@ export function useChat< devtoolsBridgeFactory: createChatDevtoolsBridge, ...transport, initialMessages: messagesToUse, - ...(initialOptions.body !== undefined && { body: initialOptions.body }), ...(typeof initialOptions.threadId === 'string' && initialOptions.persistence === true ? { @@ -207,27 +206,18 @@ export function useChat< client.getSnapshot, ) - // Sync body / forwardedProps changes to the client. - // Both populate the same wire payload; `forwardedProps` is preferred - // and `body` is deprecated but still supported. + // Sync forwardedProps changes to the client. useEffect(() => { // Conditional spread: `updateOptions` declares strict-optional // fields and rejects explicit `undefined` under EOPT. client.updateOptions({ - body: options.body, ...(options.forwardedProps !== undefined && { forwardedProps: options.forwardedProps, }), context: options.context, ...(options.queue !== undefined && { queue: options.queue }), }) - }, [ - client, - options.body, - options.forwardedProps, - options.context, - options.queue, - ]) + }, [client, options.forwardedProps, options.context, options.queue]) useEffect(() => { if (options.tools !== undefined) { @@ -365,20 +355,6 @@ export function useChat< [client], ) - const addToolApprovalResponse = useCallback( - async (response: { id: string; approved: boolean }) => { - await client.addToolApprovalResponse(response) - }, - [client], - ) - - const resumeInterrupts = useCallback( - async (resumeItems: Array, state?: ChatResumeState) => { - return await client.resumeInterrupts(resumeItems, state) - }, - [client], - ) - const resolveInterrupts = useCallback( ( resolution: @@ -430,18 +406,15 @@ export function useChat< setMessages: setMessagesManually, clear, addToolResult, - addToolApprovalResponse, queue: snapshot.queue, cancelQueued, runId: snapshot.runId, interrupts: snapshot.interruptState.interrupts, - pendingInterrupts: snapshot.interruptState.pendingInterrupts, interruptErrors: snapshot.interruptState.interruptErrors, resuming: snapshot.interruptState.resuming, resolveInterrupts, cancelInterrupts, retryInterrupts, resumeInterruptsUnsafe, - resumeInterrupts, } } diff --git a/packages/ai-preact/src/use-web-mcp-tools.ts b/packages/ai-preact/src/use-web-mcp-tools.ts index 0073c33ba0..5294cabab1 100644 --- a/packages/ai-preact/src/use-web-mcp-tools.ts +++ b/packages/ai-preact/src/use-web-mcp-tools.ts @@ -16,12 +16,6 @@ export type UseRegisterWebMCPToolsOptions< onError?: (error: unknown) => void } -/** @deprecated Use `UseRegisterWebMCPToolsOptions`. Removed in 1.0.0. */ -export type UseWebMCPToolsOptions< - TTools extends ReadonlyArray, - TContext = InferredClientContext, -> = UseRegisterWebMCPToolsOptions - type UseRegisterWebMCPToolsArguments< TTools extends ReadonlyArray, TContext, @@ -68,12 +62,6 @@ export function useRegisterWebMCPTools< }, [tools, options]) } -/** - * @deprecated Use `useRegisterWebMCPTools`. Removed in 1.0.0. - * @alias - */ -export const useWebMCPTools = useRegisterWebMCPTools - /** Options for {@link usePageWebMCPTools}. */ export type UsePageWebMCPToolsOptions = Omit< SubscribeWebMCPToolsOptions, diff --git a/packages/ai-preact/tests/use-chat.test.ts b/packages/ai-preact/tests/use-chat.test.ts index 1babc4229d..9c52dcfbac 100644 --- a/packages/ai-preact/tests/use-chat.test.ts +++ b/packages/ai-preact/tests/use-chat.test.ts @@ -62,7 +62,6 @@ describe('useChat', () => { { source: 'hydrate' }, ) expect(Object.isFrozen(result.current.interrupts)).toBe(true) - expect(result.current.pendingInterrupts).toBe(result.current.interrupts) expect(result.current.interrupts[0]).toMatchObject({ id: 'staged-interrupt', status: 'pending', @@ -261,7 +260,7 @@ describe('useChat', () => { }, ] const persistence = { - getItem: vi.fn(() => persistedMessages), + getItem: vi.fn(() => ({ messages: persistedMessages })), setItem: vi.fn(), removeItem: vi.fn(), } @@ -315,7 +314,7 @@ describe('useChat', () => { }, ] const persistence = { - getItem: vi.fn(() => []), + getItem: vi.fn(() => ({ messages: [] })), setItem: vi.fn(), removeItem: vi.fn(), } @@ -334,7 +333,7 @@ describe('useChat', () => { }) it('should ignore async persisted messages from a previous id', async () => { - const oldHydration = createDeferred>() + const oldHydration = createDeferred<{ messages: Array }>() const oldMessages: Array = [ { id: 'old-persisted', @@ -353,7 +352,7 @@ describe('useChat', () => { ] const persistence = { getItem: vi.fn((id: string) => - id === 'old-chat' ? oldHydration.promise : newMessages, + id === 'old-chat' ? oldHydration.promise : { messages: newMessages }, ), setItem: vi.fn(), removeItem: vi.fn(), @@ -381,7 +380,7 @@ describe('useChat', () => { }) await act(async () => { - oldHydration.resolve(oldMessages) + oldHydration.resolve({ messages: oldMessages }) await oldHydration.promise }) @@ -576,7 +575,7 @@ describe('useChat', () => { const { result } = renderUseChat({ connection: adapter, - body: { provider: 'openai' }, + forwardedProps: { provider: 'openai' }, }) await act(async () => { @@ -1337,16 +1336,16 @@ describe('useChat', () => { expect(result.current.messages).toBe(initialMessages) }) - it('should handle body changes', () => { + it('should handle forwardedProps changes', () => { const adapter = createMockConnectionAdapter() const { result, rerender } = renderUseChat({ connection: adapter, - body: { userId: '123' }, + forwardedProps: { userId: '123' }, }) rerender({ connection: adapter, - body: { userId: '456' }, + forwardedProps: { userId: '456' }, }) // Should not throw @@ -1855,61 +1854,6 @@ describe('useChat', () => { }) } }) - - it('should handle addToolApprovalResponse', async () => { - const toolCalls = createToolCallChunks([ - { id: 'tool-1', name: 'testTool', arguments: '{"param": "value"}' }, - ]) - const adapter = createMockConnectionAdapter({ chunks: toolCalls }) - const { result } = renderUseChat({ - connection: adapter, - }) - - await act(async () => { - await result.current.sendMessage('Test') - }) - - await waitFor(() => { - const assistantMessage = result.current.messages.find( - (m) => m.role === 'assistant', - ) - expect(assistantMessage).toBeDefined() - }) - - // Find tool call with approval - const assistantMessage = result.current.messages.find( - (m) => m.role === 'assistant', - ) - const toolCallPart = assistantMessage?.parts.find( - (p) => p.type === 'tool-call' && p.approval, - ) - - if ( - toolCallPart && - toolCallPart.type === 'tool-call' && - toolCallPart.approval - ) { - await act(async () => { - await result.current.addToolApprovalResponse({ - id: toolCallPart.approval!.id, - approved: true, - }) - }) - - // Should update approval state - await waitFor(() => { - const updatedMessage = result.current.messages.find( - (m) => m.role === 'assistant', - ) - const updatedToolCall = updatedMessage?.parts.find( - (p) => p.type === 'tool-call' && p.id === toolCallPart.id, - ) - if (updatedToolCall && updatedToolCall.type === 'tool-call') { - expect(updatedToolCall.approval?.approved).toBe(true) - } - }) - } - }) }) describe('sessionGenerating', () => { diff --git a/packages/ai-preact/tests/use-web-mcp-tools.test.ts b/packages/ai-preact/tests/use-web-mcp-tools.test.ts index c1275061af..632fd22b94 100644 --- a/packages/ai-preact/tests/use-web-mcp-tools.test.ts +++ b/packages/ai-preact/tests/use-web-mcp-tools.test.ts @@ -2,11 +2,7 @@ import { act, renderHook, waitFor } from '@testing-library/preact' import { toolDefinition } from '@tanstack/ai/client' import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest' -import { - usePageWebMCPTools, - useRegisterWebMCPTools, - useWebMCPTools, -} from '../src' +import { usePageWebMCPTools, useRegisterWebMCPTools } from '../src' import type { AnyClientTool } from '@tanstack/ai/client' import type { UseRegisterWebMCPToolsOptions } from '../src' @@ -214,12 +210,6 @@ describe('useRegisterWebMCPTools', () => { }) }) -describe('useWebMCPTools', () => { - it('is a deprecated alias of useRegisterWebMCPTools', () => { - expect(useWebMCPTools).toBe(useRegisterWebMCPTools) - }) -}) - function installPageTools(names: Array) { const target = new EventTarget() const modelContext = { diff --git a/packages/ai-react-ui/CHANGELOG.md b/packages/ai-react-ui/CHANGELOG.md deleted file mode 100644 index 7accd07317..0000000000 --- a/packages/ai-react-ui/CHANGELOG.md +++ /dev/null @@ -1,72 +0,0 @@ -# @tanstack/ai-react-ui - -## 0.9.8 - -### Patch Changes - -- Updated dependencies [[`ef6c44f`](https://github.com/TanStack/ai/commit/ef6c44f75dd0e5c1f9187a2093d9fa1347acb85b)]: - - @tanstack/ai-react@0.31.0 - -## 0.9.7 - -### Patch Changes - -- Updated dependencies [[`807c5e1`](https://github.com/TanStack/ai/commit/807c5e11dd1560e0fe47ae2dbc90f2513507f7cc)]: - - @tanstack/ai-react@0.30.0 - -## 0.9.6 - -### Patch Changes - -- Updated dependencies [[`012fb0a`](https://github.com/TanStack/ai/commit/012fb0af0d9a3f4bf7e450882c41f0394571248d)]: - - @tanstack/ai-react@0.29.0 - -## 0.9.5 - -### Patch Changes - -- Updated dependencies [[`9ab4f76`](https://github.com/TanStack/ai/commit/9ab4f7691f39884eebe8153caa9653926ae12fd0)]: - - @tanstack/ai-react@0.28.0 - -## 0.9.4 - -### Patch Changes - -- Updated dependencies [[`62ba217`](https://github.com/TanStack/ai/commit/62ba217d31231fe3ff43ae71b649e047d3a6c1b1)]: - - @tanstack/ai-react@0.27.0 - -## 0.9.3 - -### Patch Changes - -- Updated dependencies [[`0945a79`](https://github.com/TanStack/ai/commit/0945a79b0923b31a5122d0bf28c115879341a410)]: - - @tanstack/ai-react@0.26.0 - -## 0.9.2 - -### Patch Changes - -- Updated dependencies [[`5ae2cd2`](https://github.com/TanStack/ai/commit/5ae2cd2c3c4e2cabf2758a1a2eca9c14d44cb626)]: - - @tanstack/ai-react@0.25.0 - -## 0.9.1 - -### Patch Changes - -- Updated dependencies [[`82ced0f`](https://github.com/TanStack/ai/commit/82ced0f5018297e5756828ecc4d312ba78adeaab)]: - - @tanstack/ai-react@0.24.0 - -## 0.9.0 - -### Minor Changes - -- [#1252](https://github.com/TanStack/ai/pull/1252) [`a4ab03e`](https://github.com/TanStack/ai/commit/a4ab03e213dcda2cd1120c9e7bf4824650996fae) - Deprecated. Import chat UI from `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, or `@tanstack/ai-vue/ui` instead. - - These packages re-export the new `/ui` subpath until 1.0.0. `npm install` prints a deprecation warning. - - `@tanstack/ai-svelte-ui` was never published. Svelte chat UI lives only on `@tanstack/ai-svelte/ui`. - -### Patch Changes - -- Updated dependencies [[`a4ab03e`](https://github.com/TanStack/ai/commit/a4ab03e213dcda2cd1120c9e7bf4824650996fae), [`a4ab03e`](https://github.com/TanStack/ai/commit/a4ab03e213dcda2cd1120c9e7bf4824650996fae)]: - - @tanstack/ai-react@0.23.0 diff --git a/packages/ai-react-ui/LICENSE b/packages/ai-react-ui/LICENSE deleted file mode 100644 index 308cb68dc0..0000000000 --- a/packages/ai-react-ui/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 Tanner Linsley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/ai-react-ui/README.md b/packages/ai-react-ui/README.md deleted file mode 100644 index 180a9dd022..0000000000 --- a/packages/ai-react-ui/README.md +++ /dev/null @@ -1,29 +0,0 @@ -
- - - - TanStack AI - -
- -
- -# @tanstack/ai-react-ui (deprecated) - -This package is deprecated since 0.9.0. It will be removed in 1.0.0. - -Import chat UI from [`@tanstack/ai-react/ui`](https://tanstack.com/ai/latest/docs/ui/react) instead. - -This package re-exports that subpath so existing installs keep working during the move. - -See the [migration guide](https://tanstack.com/ai/latest/docs/migration/create-ui). diff --git a/packages/ai-react-ui/package.json b/packages/ai-react-ui/package.json deleted file mode 100644 index 2f86aad084..0000000000 --- a/packages/ai-react-ui/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@tanstack/ai-react-ui", - "version": "0.9.8", - "description": "Deprecated. Import chat UI from @tanstack/ai-react/ui instead.", - "author": "Tanner Linsley", - "license": "MIT", - "homepage": "https://tanstack.com/ai", - "repository": { - "type": "git", - "url": "git+https://github.com/TanStack/ai.git", - "directory": "packages/ai-react-ui" - }, - "bugs": { - "url": "https://github.com/TanStack/ai/issues" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "deprecated": "Use @tanstack/ai-react/ui instead. See https://tanstack.com/ai/latest/docs/migration/create-ui", - "module": "./dist/esm/index.js", - "types": "./dist/esm/index.d.ts", - "type": "module", - "exports": { - ".": { - "types": "./dist/esm/index.d.ts", - "import": "./dist/esm/index.js" - } - }, - "files": [ - "dist", - "src" - ], - "scripts": { - "build": "vite build", - "clean": "premove ./build ./dist", - "test:build": "publint --strict", - "test:oxlint": "oxlint src --type-aware", - "test:types": "tsc" - }, - "keywords": [ - "ai", - "tanstack", - "react", - "chat", - "ui", - "deprecated" - ], - "dependencies": { - "@tanstack/ai-react": "workspace:^" - }, - "devDependencies": { - "@types/react": "^19.2.7", - "react": "^19.2.3", - "vite": "^8.2.1" - } -} diff --git a/packages/ai-react-ui/src/index.ts b/packages/ai-react-ui/src/index.ts deleted file mode 100644 index 033d0f45d8..0000000000 --- a/packages/ai-react-ui/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @deprecated Since 0.9.0. Import from `@tanstack/ai-react/ui` instead. - * See https://tanstack.com/ai/latest/docs/migration/create-ui - * Removed in 1.0.0. - */ -export * from '@tanstack/ai-react/ui' diff --git a/packages/ai-react-ui/tsconfig.json b/packages/ai-react-ui/tsconfig.json deleted file mode 100644 index aae0726181..0000000000 --- a/packages/ai-react-ui/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "jsx": "react-jsx", - "lib": ["ES2022", "DOM"], - "rootDir": "../.." - }, - "include": ["src"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/ai-react-ui/vite.config.ts b/packages/ai-react-ui/vite.config.ts deleted file mode 100644 index 190315c4bf..0000000000 --- a/packages/ai-react-ui/vite.config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { defineConfig, mergeConfig } from 'vitest/config' -import { tanstackViteConfig } from '@tanstack/vite-config' -import packageJson from './package.json' - -const config = defineConfig({ - test: { - name: packageJson.name, - dir: './', - watch: false, - globals: true, - environment: 'node', - include: ['tests/**/*.test.ts'], - passWithNoTests: true, - }, -}) - -export default mergeConfig( - config, - tanstackViteConfig({ - entry: ['./src/index.ts'], - srcDir: './src', - cjs: false, - }), -) diff --git a/packages/ai-react/src/chat-ui/chat-input.tsx b/packages/ai-react/src/chat-ui/chat-input.tsx deleted file mode 100644 index 3811f92de9..0000000000 --- a/packages/ai-react/src/chat-ui/chat-input.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { useRef, useState } from 'react' -import { useChatContext } from './chat' -import type { ReactNode, RefObject } from 'react' - -export interface ChatInputRenderProps { - /** Current input value */ - value: string - /** Set input value */ - onChange: (value: string) => void - /** Submit the message */ - onSubmit: () => void - /** Is the chat currently loading */ - isLoading: boolean - /** Is input disabled */ - disabled: boolean - /** Ref to the input element */ - inputRef: RefObject -} - -export interface ChatInputProps { - /** Render prop for full control */ - children?: (props: ChatInputRenderProps) => ReactNode - /** CSS class name */ - className?: string - /** Placeholder text */ - placeholder?: string - /** Disable input */ - disabled?: boolean - /** Submit on Enter (Shift+Enter for new line) */ - submitOnEnter?: boolean -} - -/** - * @deprecated Use `createChatUI()` and an application-owned input component. - * Deprecated in 0.9.0. Removed in 1.0.0. - * - * Chat input component - handles message input and submission - * - * Features: - * - Auto-growing textarea - * - Submit on Enter (Shift+Enter for new line) - * - Loading state management - * - Full render prop support for custom UIs - * - * @example - * ```tsx - * - * ``` - * - * @example Custom UI with render prop - * ```tsx - * - * {({ value, onChange, onSubmit, isLoading }) => ( - *
- *