Conversation
📝 WalkthroughWalkthroughThis PR enforces alternating roles and deduplicates tool/function call blocks for Anthropic and Gemini adapters, enhances Anthropic stream lifecycle signaling (tool run/start/finish and text boundaries), adds Claude Opus 4.6 model + provider options, adds tests for multi-turn tool flows, updates several example/test dependency versions, and adds a version-fix script. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Adapter
participant Provider as Anthropic API
participant Tool
Client->>Adapter: send messages (may contain tool_use/tool_result)
Adapter->>Adapter: mergeConsecutiveSameRoleMessages()\ndedupe tool blocks\nformatMessages()
Adapter->>Provider: start streaming (RUN_STARTED)
Provider-->>Adapter: stream chunks (text/content_block_start)
Adapter->>Adapter: on content_block_start -> emit TOOL_CALL_START / TEXT_MESSAGE_START
Provider-->>Adapter: tool content / tool_result chunks
Adapter->>Tool: parse tool_result -> TOOL_CALL_END (include parsedInput)
Provider-->>Adapter: content_block_stop / message_stop
Adapter->>Adapter: emit TEXT_MESSAGE_END / STEP_FINISHED as appropriate
Adapter->>Provider: when done -> emit RUN_FINISHED (guarded)
Adapter-->>Client: SSE stream of lifecycle events & final content
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/typescript/ai-anthropic/src/text/text-provider-options.ts (1)
147-156:⚠️ Potential issue | 🔴 CriticalFix type conflict:
{ type: 'adaptive' }is unassignable due to intersection withAnthropicThinkingOptions.
ExternalTextProviderOptionsat line 152 intersectsAnthropicThinkingOptionswithPartial<AnthropicAdaptiveThinkingOptions>at line 155. Both define athinkingproperty with conflicting discriminated union types. TypeScript computes the intersection of these types — only discriminator variants present in BOTH unions are assignable. Since{ type: 'adaptive' }exists only inAnthropicAdaptiveThinkingOptions, it cannot satisfy the intersection, preventing users from passing adaptive thinking configuration despite it being declared in the interfaces.Replace
AnthropicThinkingOptionswithAnthropicAdaptiveThinkingOptions(which is a superset containing all variants including'adaptive') to preserve type safety for all thinking modes:Proposed fix
export type ExternalTextProviderOptions = AnthropicContainerOptions & AnthropicContextManagementOptions & AnthropicMCPOptions & AnthropicServiceTierOptions & AnthropicStopSequencesOptions & - AnthropicThinkingOptions & + AnthropicAdaptiveThinkingOptions & AnthropicToolChoiceOptions & AnthropicSamplingOptions & - Partial<AnthropicAdaptiveThinkingOptions> & Partial<AnthropicEffortOptions>
🤖 Fix all issues with AI agents
In `@packages/typescript/ai-anthropic/src/adapters/text.ts`:
- Around line 684-695: The code emits a TEXT_MESSAGE_END for any
content_block_stop where currentBlockType !== 'tool_use', which can spuriously
include non-text types like 'thinking'; modify the guard so the yield of { type:
'TEXT_MESSAGE_END', ... } only happens when the block that just ended was
actually a text block (e.g., check currentBlockType === 'text' or explicitly
match the set of text block types) in addition to hasEmittedTextMessageStart and
accumulatedContent; update the conditional around the yield in the same block
where hasEmittedTextMessageStart, accumulatedContent, and currentBlockType are
referenced (symbols: currentBlockType, hasEmittedTextMessageStart,
accumulatedContent, and the TEXT_MESSAGE_END yield) so TEXT_MESSAGE_END is not
emitted for non-text/non-tool_use block types.
In `@testing/panel/package.json`:
- Around line 24-27: Remove the legacy package entry "@tanstack/start" from
dependencies and update the TanStack package versions to be consistent with
"@tanstack/react-start"; specifically, delete the "@tanstack/start": "^1.120.20"
line and change "@tanstack/react-router": "^1.158.4" to "^1.159.0" so it matches
"@tanstack/react-start": "^1.159.0" (leave "@tanstack/nitro-v2-vite-plugin"
unchanged unless other TanStack version constraints require alignment).
🧹 Nitpick comments (7)
scripts/fix-version-bump.ts (4)
78-78: UseArray<T>generic syntax per project lint rules.ESLint reports
@typescript-eslint/array-typeviolations on these two lines.🔧 Proposed fix
- const packagesToFix: PackageToFix[] = [] + const packagesToFix: Array<PackageToFix> = []- const errors: string[] = [] + const errors: Array<string> = []Also applies to: 108-108
146-150: Commit message guidance uses a hardcoded placeholder.Line 149 suggests committing with
"fix: correct version bump to X.Y.Z"but at this point the actual target version is known. Consider interpolating the resolved version for a more helpful message.🔧 Proposed fix
+ const resolvedVersion = cliVersion || packagesToFix[0].detectedVersion! console.log('\n✅ Done! Version bump fixed.') console.log('\nNext steps:') console.log(' 1. Review the changes: git diff') - console.log(' 2. Commit: git add -A && git commit -m "fix: correct version bump to X.Y.Z"') + console.log(` 2. Commit: git add -A && git commit -m "fix: correct version bump to ${resolvedVersion}"`)
14-23: CLI flag value joined with=is not handled.
parseArgsonly supports--version X.Y.Z(space-separated), but common CLI convention also allows--version=X.Y.Z. Users passing the=form will silently getversion: nulland the script will fall back to auto-detection or error out.🔧 Proposed fix
function parseArgs(): { version: string | null } { const args = process.argv.slice(2) - const versionIndex = args.findIndex((arg) => arg === '--version' || arg === '-v') - - if (versionIndex !== -1 && args[versionIndex + 1]) { - return { version: args[versionIndex + 1] } + for (const arg of args) { + if (arg.startsWith('--version=') || arg.startsWith('-v=')) { + return { version: arg.split('=')[1] || null } + } + } + const versionIndex = args.findIndex((a) => a === '--version' || a === '-v') + if (versionIndex !== -1 && args[versionIndex + 1]) { + return { version: args[versionIndex + 1] } } - return { version: null } }
49-56: Regex-based JSON editing is fragile.
fixPackageJsonuses a regex to replace the version string in raw JSON text. This works for typicalpackage.jsonfiles but could silently corrupt files with unusual formatting or multiple"version": "1.0.0"fields (e.g., in nested metadata). Since this is a one-off internal script rather than a library, the risk is low—just worth noting.packages/typescript/ai-anthropic/src/text/text-provider-options.ts (1)
207-217:validateThinkingdoesn't handletype: 'adaptive'.The validation function only handles
type: 'enabled'and would silently skiptype: 'adaptive'. If adaptive thinking has any constraints (e.g., it shouldn't coexist withbudget_tokens), they aren't enforced here. If no validation is needed for adaptive mode, a brief comment explaining that would help maintainability.packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts (1)
399-511: Thorough reproduction of the real-world multi-turn scenario.Minor note:
tools: [weatherTool](line 477) doesn't match the tool names used in the messages (getGuitars,recommendGuitar). This doesn't affect correctness since the test focuses on message formatting/deduplication, not tool dispatch — but aligning the tools would improve readability if this test is ever used as a reference for future debugging.packages/typescript/ai-anthropic/src/adapters/text.ts (1)
696-708:hasEmittedRunFinishednaming is slightly misleading for themax_tokenscase.When
stop_reasonismax_tokens, the code emitsRUN_ERROR(notRUN_FINISHED), but the flag namehasEmittedRunFinishedstill prevents a follow-upRUN_FINISHEDfrommessage_stop. The behavior is correct — we shouldn't emit both — but a name likehasEmittedTerminalEventwould better express intent. Minor nit, no functional issue.
| } else { | ||
| // Emit TEXT_MESSAGE_END only for text blocks (not tool_use blocks) | ||
| if (hasEmittedTextMessageStart && accumulatedContent) { | ||
| yield { | ||
| type: 'TEXT_MESSAGE_END', | ||
| messageId, | ||
| model, | ||
| timestamp, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Emit TEXT_MESSAGE_END if we had text content | ||
| if (hasEmittedTextMessageStart && accumulatedContent) { | ||
| currentBlockType = null |
There was a problem hiding this comment.
TEXT_MESSAGE_END may emit spuriously for non-text, non-tool_use block types (e.g., thinking).
The else branch fires for any content_block_stop where currentBlockType !== 'tool_use', which includes thinking blocks. If a text block preceded the thinking block, this would emit a spurious TEXT_MESSAGE_END when the thinking block stops — because hasEmittedTextMessageStart and accumulatedContent are both still truthy.
In practice Anthropic puts thinking blocks before text blocks, so this isn't currently triggered. But if the response ordering changes or new block types are added, it could surface.
Suggested guard
} else {
- // Emit TEXT_MESSAGE_END only for text blocks (not tool_use blocks)
- if (hasEmittedTextMessageStart && accumulatedContent) {
+ // Emit TEXT_MESSAGE_END only when a text block ends
+ if (
+ currentBlockType === 'text' &&
+ hasEmittedTextMessageStart &&
+ accumulatedContent
+ ) {
yield {
type: 'TEXT_MESSAGE_END',
messageId,
model,
timestamp,
}
}
}🤖 Prompt for AI Agents
In `@packages/typescript/ai-anthropic/src/adapters/text.ts` around lines 684 -
695, The code emits a TEXT_MESSAGE_END for any content_block_stop where
currentBlockType !== 'tool_use', which can spuriously include non-text types
like 'thinking'; modify the guard so the yield of { type: 'TEXT_MESSAGE_END',
... } only happens when the block that just ended was actually a text block
(e.g., check currentBlockType === 'text' or explicitly match the set of text
block types) in addition to hasEmittedTextMessageStart and accumulatedContent;
update the conditional around the yield in the same block where
hasEmittedTextMessageStart, accumulatedContent, and currentBlockType are
referenced (symbols: currentBlockType, hasEmittedTextMessageStart,
accumulatedContent, and the TEXT_MESSAGE_END yield) so TEXT_MESSAGE_END is not
emitted for non-text/non-tool_use block types.
| "@tanstack/nitro-v2-vite-plugin": "^1.154.7", | ||
| "@tanstack/react-router": "^1.158.4", | ||
| "@tanstack/react-start": "^1.159.0", | ||
| "@tanstack/start": "^1.120.20", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if `@tanstack/start` is used elsewhere and what versions are pinned
rg '"@tanstack/start"' --type json -nRepository: TanStack/ai
Length of output: 120
🌐 Web query:
@tanstack/start vs @tanstack/react-start version alignment requirements
💡 Result:
-
Don’t try to “align”
@tanstack/startwith@tanstack/react-start: they’re effectively different generations of TanStack Start. The current docs list React support as@tanstack/react-start(and Solid as@tanstack/solid-start) rather than using@tanstack/startdirectly. [2] A commonly referenced migration path is to remove@tanstack/startand install@tanstack/react-start. [3] -
@tanstack/react-startexpects same-version TanStack Router/Start internals: the package has hard-pinned dependencies (not ranges) on@tanstack/react-routerand multiple Start core packages (eg@tanstack/react-router: "1.158.1"when@tanstack/react-startis1.158.1). This means you should keep these TanStack packages on the exact same version (or let your package manager resolve them via@tanstack/react-start), otherwise you risk duplicate/mismatched installs. [1] -
Practical rule
- If you’re on modern Start: install/upgrade as a matched set:
@tanstack/react-start==@tanstack/react-router(and any other@tanstack/*start*packages you add). [1] - If you still have legacy
@tanstack/startin a project, treat it as a migration candidate, not something to version-match with@tanstack/react-start. [3]
- If you’re on modern Start: install/upgrade as a matched set:
Sources:
[1] @tanstack/react-start package.json (deps pinned to same version) https://app.unpkg.com/%40tanstack/react-start%401.158.1/files/package.json
[2] TanStack Start docs – supported frameworks (React uses @tanstack/react-start) https://tanstack.com/start/v0/docs/framework
[3] LogRocket migration article (remove @tanstack/start, install @tanstack/react-start) https://blog.logrocket.com/migrating-tanstack-start-vinxi-vite/
🏁 Script executed:
cat -n testing/panel/package.jsonRepository: TanStack/ai
Length of output: 1958
Remove legacy @tanstack/start in favor of modern @tanstack/react-start.
The repository includes both @tanstack/start (^1.120.20) and @tanstack/react-start (^1.159.0). According to TanStack's migration guidance, @tanstack/start is a legacy package; modern React projects should use only @tanstack/react-start. Additionally, @tanstack/react-start has hard-pinned dependencies on matching versions of related TanStack packages—ensure @tanstack/react-router (currently ^1.158.4) is kept at the same version as @tanstack/react-start (^1.159.0) to avoid duplicate or mismatched installs.
🤖 Prompt for AI Agents
In `@testing/panel/package.json` around lines 24 - 27, Remove the legacy package
entry "@tanstack/start" from dependencies and update the TanStack package
versions to be consistent with "@tanstack/react-start"; specifically, delete the
"@tanstack/start": "^1.120.20" line and change "@tanstack/react-router":
"^1.158.4" to "^1.159.0" so it matches "@tanstack/react-start": "^1.159.0"
(leave "@tanstack/nitro-v2-vite-plugin" unchanged unless other TanStack version
constraints require alignment).
|
| Command | Status | Duration | Result |
|---|---|---|---|
nx affected --targets=test:sherif,test:knip,tes... |
❌ Failed | 3m 6s | View ↗ |
nx run-many --targets=build --exclude=examples/** |
✅ Succeeded | 1m 14s | View ↗ |
☁️ Nx Cloud last updated this comment at 2026-02-07 21:24:37 UTC
@tanstack/ai
@tanstack/ai-anthropic
@tanstack/ai-client
@tanstack/ai-devtools-core
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-openrouter
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@packages/typescript/ai-openrouter/src/adapters/text.ts`:
- Around line 114-138: Remove all local debug instrumentation in the chatStream
flow: delete the fetch-based "agent log" blocks and their surrounding
//#region...//#endregion comments inside the chatStream implementation, and
remove the related chunkCountOuter and chunkCount tracking variables; ensure any
references to these variables are also removed or replaced. Specifically, inside
the chatStream method (and any helper logic that calls mapTextOptionsToSDK)
remove the hardcoded fetch calls to 127.0.0.1:7244/ingest and the comment
regions, then run the linter/tests to ensure no leftover references to
chunkCountOuter, chunkCount, or the debug blocks remain.
In `@testing/panel/src/routes/api.chat.ts`:
- Around line 152-171: Remove all temporary debug "agent log" blocks that
perform fire-and-forget fetches to http://127.0.0.1:7244/ingest/... within this
file; specifically delete each // `#region` agent log ... // `#endregion` block (the
POST handler entry block around the POST handler, and the similar blocks near
lines referenced) so no hardcoded fetches or leaked internal data remain; search
for the exact region marker string "// `#region` agent log" and the fetch URL to
find every occurrence (e.g., the block inside the POST handler that logs
'[DEBUG] POST /api/chat handler entered') and remove them, and if persistent
telemetry is required replace them with calls to the app’s structured
logging/observability API instead of direct fetches.
🧹 Nitpick comments (4)
scripts/fix-version-bump.ts (2)
77-77: UseArray<T>generic syntax per project ESLint config.The project's
@typescript-eslint/array-typerule requires the generic form.♻️ Proposed fix
- const packagesToFix: PackageToFix[] = [] + const packagesToFix: Array<PackageToFix> = []- const errors: string[] = [] + const errors: Array<string> = []Also applies to: 110-110
14-25: Consider validating the CLI-provided version string.If a user passes a non-semver string (e.g.,
--version oops), it will be written verbatim intopackage.jsonandCHANGELOG.md. A simple regex check would guard against typos.🛡️ Suggested guard
function parseArgs(): { version: string | null } { const args = process.argv.slice(2) const versionIndex = args.findIndex( (arg) => arg === '--version' || arg === '-v', ) if (versionIndex !== -1 && args[versionIndex + 1]) { - return { version: args[versionIndex + 1] } + const version = args[versionIndex + 1] + if (!/^\d+\.\d+\.\d+/.test(version)) { + console.error(`❌ Invalid version format: ${version}`) + process.exit(1) + } + return { version } } return { version: null } }testing/panel/src/routes/api.chat.ts (1)
267-294: Wrappingchat()in try/catch is fine, but the catch only re-throws.Without the debug logging (which should be removed), the inner try/catch on lines 268–319 serves no purpose — it catches
chatErrorand immediately re-throws it, which the outercatchon line 345 already handles.If the intent is to add provider-specific error handling or transformation, implement that logic here; otherwise, remove the redundant try/catch to keep the code straightforward.
Proposed simplification (after removing debug logging)
- let stream: AsyncIterable<any> - try { - stream = chat({ + const stream = chat({ ...options, adapter, tools: [ getGuitars, recommendGuitarToolDef, addToCartToolServer, addToWishListToolDef, getPersonalGuitarPreferenceToolDef, ], systemPrompts: [SYSTEM_PROMPT], agentLoopStrategy: maxIterations(20), messages, modelOptions: {}, abortController, - }) - } catch (chatError: any) { - throw chatError - } + })packages/typescript/ai-openrouter/src/adapters/text.ts (1)
139-168: The inner try/catch aroundchat.sendis a good pattern — keep it, but clean it up.Wrapping the
await this.client.chat.send(...)call in its own try/catch (lines 140–168) is useful for distinguishing SDK initialization errors from stream-processing errors. After removing the debug logging, simplify to just re-throw or add meaningful error wrapping:Proposed cleanup
let stream: AsyncIterable<any> try { stream = await this.client.chat.send( { ...requestParams, stream: true }, { signal: options.request?.signal }, ) } catch (sendError: any) { - // `#region` agent log - fetch( - 'http://127.0.0.1:7244/ingest/830522ab-8098-40b9-a021-890f9c041588', - { ... }, - ).catch(() => {}) - // `#endregion` throw sendError }
| let chunkCountOuter = 0 | ||
| try { | ||
| const requestParams = this.mapTextOptionsToSDK(options) | ||
| const stream = await this.client.chat.send( | ||
| { ...requestParams, stream: true }, | ||
| { signal: options.request?.signal }, | ||
| ) | ||
| // #region agent log | ||
| fetch( | ||
| 'http://127.0.0.1:7244/ingest/830522ab-8098-40b9-a021-890f9c041588', | ||
| { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| location: 'openrouter/text.ts:chatStream', | ||
| message: 'chatStream entry - request params', | ||
| data: { | ||
| model: requestParams.model, | ||
| messageCount: requestParams.messages?.length, | ||
| hasTools: !!requestParams.tools, | ||
| toolCount: requestParams.tools?.length, | ||
| keys: Object.keys(requestParams), | ||
| }, | ||
| timestamp: Date.now(), | ||
| hypothesisId: 'C', | ||
| }), | ||
| }, | ||
| ).catch(() => {}) | ||
| // #endregion |
There was a problem hiding this comment.
Critical: Remove all debug instrumentation from this published package.
This file is part of @tanstack/ai-openrouter — a published npm package. The hardcoded fetch calls to http://127.0.0.1:7244/ingest/... would execute for every consumer of this adapter. Even though they're fire-and-forget with .catch(() => {}), they:
- Generate unnecessary network requests on every
chatStreamcall and on every streamed chunk (lines 176–202, 235–262) - Leak internal diagnostic data (error stacks, model params, content lengths) to a localhost endpoint
- Introduce latency and resource overhead in production
All // #regionagent log … //#endregion`` blocks (lines 117–138, 146–166, 174–203, 233–263, 287–308, 310–330) and the supporting chunkCountOuter / `chunkCount` tracking variables (lines 114, 170, 172–173) that exist solely for this logging must be removed before merge.
🤖 Prompt for AI Agents
In `@packages/typescript/ai-openrouter/src/adapters/text.ts` around lines 114 -
138, Remove all local debug instrumentation in the chatStream flow: delete the
fetch-based "agent log" blocks and their surrounding //#region...//#endregion
comments inside the chatStream implementation, and remove the related
chunkCountOuter and chunkCount tracking variables; ensure any references to
these variables are also removed or replaced. Specifically, inside the
chatStream method (and any helper logic that calls mapTextOptionsToSDK) remove
the hardcoded fetch calls to 127.0.0.1:7244/ingest and the comment regions, then
run the linter/tests to ensure no leftover references to chunkCountOuter,
chunkCount, or the debug blocks remain.
| // #region agent log | ||
| console.log( | ||
| '[DEBUG] POST /api/chat handler entered at', | ||
| new Date().toISOString(), | ||
| ) | ||
| fetch( | ||
| 'http://127.0.0.1:7244/ingest/830522ab-8098-40b9-a021-890f9c041588', | ||
| { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| location: 'api.chat.ts:POST:ENTRY', | ||
| message: 'POST handler entered', | ||
| data: { url: request.url, method: request.method }, | ||
| timestamp: Date.now(), | ||
| hypothesisId: 'ENTRY0', | ||
| }), | ||
| }, | ||
| ).catch(() => {}) | ||
| // #endregion |
There was a problem hiding this comment.
Remove debug instrumentation before merging to main.
These fire-and-forget fetch calls to a hardcoded http://127.0.0.1:7244/ingest/... endpoint are clearly temporary debugging artifacts. They appear in multiple regions throughout this file (lines 152–171, 226–249, 295–319, 321–342). Shipping them to main adds dead code that will silently fail in any environment without that local service, clutters the handler, and leaks internal details (e.g. errorStack) to an uncontrolled endpoint.
Please strip all // #regionagent log … //#endregion`` blocks from this file before merge. If persistent telemetry is desired, consider a proper structured logging or observability solution.
🤖 Prompt for AI Agents
In `@testing/panel/src/routes/api.chat.ts` around lines 152 - 171, Remove all
temporary debug "agent log" blocks that perform fire-and-forget fetches to
http://127.0.0.1:7244/ingest/... within this file; specifically delete each //
`#region` agent log ... // `#endregion` block (the POST handler entry block around
the POST handler, and the similar blocks near lines referenced) so no hardcoded
fetches or leaked internal data remain; search for the exact region marker
string "// `#region` agent log" and the fetch URL to find every occurrence (e.g.,
the block inside the POST handler that logs '[DEBUG] POST /api/chat handler
entered') and remove them, and if persistent telemetry is required replace them
with calls to the app’s structured logging/observability API instead of direct
fetches.

🎯 Changes
This fixes tool calls in the anthropic adapter.
✅ Checklist
pnpm run test:pr.🚀 Release Impact
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Chores