refactor: migrate ai-groq + ai-openrouter onto @tanstack/openai-base (#543)#545
Conversation
…543) Adds protected `callChatCompletion`, `callChatCompletionStream`, `extractReasoning`, and `transformStructuredOutput` hooks to `OpenAICompatibleChatCompletionsTextAdapter` so providers with non-OpenAI SDK shapes can reuse the shared stream accumulator, partial-JSON tool-call buffer, RUN_ERROR taxonomy, and lifecycle gates. ai-groq drops `groq-sdk` in favour of the OpenAI SDK pointed at api.groq.com/openai/v1; ai-openrouter keeps `@openrouter/sdk` via hook overrides. ai-ollama remains on BaseTextAdapter (native API has a different wire format). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ 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 |
🚀 Changeset Version Preview4 package(s) bumped directly, 24 bumped as dependents. 🟨 Minor bumps
🟩 Patch bumps
|
|
| Command | Status | Duration | Result |
|---|---|---|---|
nx affected --targets=test:sherif,test:knip,tes... |
❌ Failed | 6m 17s | View ↗ |
nx run-many --targets=build --exclude=examples/** |
✅ Succeeded | 1m 50s | View ↗ |
☁️ Nx Cloud last updated this comment at 2026-05-11 12:34:06 UTC
@tanstack/ai
@tanstack/ai-anthropic
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-skills
@tanstack/ai-devtools-core
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@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-utils
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
…ons migration Addresses regressions and pre-existing silent failures surfaced by reviewing #545: - `@tanstack/ai`: `toRunErrorPayload` normalizes `AbortError` / `APIUserAbortError` / `RequestAbortedError` to `{ code: 'aborted' }` so consumers can discriminate user-initiated cancellation without matching provider-specific message strings. - `@tanstack/openai-base`: `structuredOutput` throws a distinct "response contained no content" error instead of cascading into a misleading JSON-parse error on an empty string; the post-loop tool-args drain now logs malformed JSON via `logger.errors` so truncated streams don't silently invoke tools with `{}`. - `@tanstack/ai-openrouter`: `stream_options.include_usage` is camelCased to `includeUsage` (Zod was silently stripping it, leaving `RUN_FINISHED.usage` always undefined on streaming); mid-stream `chunk.error.code` is stringified so provider codes (401/429/500) survive `toRunErrorPayload`; assistant `toolCalls[].function.arguments` is stringified to match the SDK's `string` contract; `convertMessage` now mirrors the base's fail-loud guards (empty user content, unsupported content parts). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds OpenRouterResponsesTextAdapter on top of @tanstack/openai-base's responses-text base, mirroring the chat-completions migration in #543. - openai-base: protected `callResponse` / `callResponseStream` hooks on OpenAICompatibleResponsesTextAdapter parallel to the existing `callChatCompletion*` hooks, so providers whose SDK has a different call shape can override without forking processStreamChunks. Re-exports the OpenAI Responses SDK types subclasses need. - ai-openrouter: new OpenRouterResponsesTextAdapter routing through `client.beta.responses.send({ responsesRequest })`. Emits the SDK's camelCase TS shape directly via overrides of convertMessagesToInput / convertContentPartToInput / mapOptionsToRequest, annotated with `Pick<ResponsesRequest, ...>` so future SDK field renames break the build instead of silently producing Zod-stripped wire payloads. Bridges inbound stream events camel -> snake so the base's processStreamChunks reads documented fields unchanged. - Function tools only in v1; webSearchTool() throws with a clear error pointing at the chat-completions adapter. - Folds in the silent-failure lessons from 0171b18 (stringified error codes, stringified tool-call arguments, fail-loud on empty user content). - E2E: new `openrouter-responses` provider slot in feature-support / test-matrix / providers / types / api.summarize, reusing aimock's native `/v1/responses` handler. - 10 new unit tests covering request mapping (snake -> camel for top-level fields, function-call camelCasing in input[], variant suffix), stream-event bridge (text deltas, function-call lifecycle, response.failed, top-level error code stringification), webSearchTool() rejection, and SDK constructor wiring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes `validateTextProviderOptions` (no-op stub never called) and the chain of `ChatCompletion*MessageParam` / `ChatCompletionContentPart*` / `ChatCompletionMessageToolCall` types that were only referenced by it. Unblocks the root `test:knip` CI check. None of the removed exports are re-exported from the package's public `src/index.ts`, so this is internal-only cleanup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The OpenRouter SDK's stream-event schema is built with Speakeasy's
discriminated-union helper, which on a per-variant parse failure falls
back to `{ raw, type: 'UNKNOWN', isUnknown: true }` rather than throwing.
This happens whenever an upstream omits an "optional-looking" required
field — notably `sequence_number` and `logprobs` on text/reasoning delta
events, which aimock-served fixtures don't include.
Before this fix the adapter's switch hit the default branch for UNKNOWN
events and emitted them with no usable `type`, so the base's
processStreamChunks ignored them silently — the run terminated as
`RUN_FINISHED { finishReason: 'stop' }` with no content.
The `raw` payload preserved on the fallback is the original wire-shape
event in snake_case, which is exactly what processStreamChunks reads.
Re-emit it verbatim. Real-OpenRouter responses still flow through the
existing camel -> snake bridge because their events include the required
fields and parse cleanly.
Unblocks the openrouter-responses E2E suite: 11 affected tests now pass
locally against aimock; before this commit they all timed out empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…i-openrouter-ai-ollama-to-openai-base-+-parameterize-the-base-for-sdk-shape-variance

Summary
Closes #543 (for groq + openrouter; ollama stays on
BaseTextAdapter— see rationale below).@tanstack/openai-base— adds four protected hooks onOpenAICompatibleChatCompletionsTextAdapterso providers with non-OpenAI SDK shapes can plug in:callChatCompletion/callChatCompletionStream(SDK call sites),extractReasoning(surface reasoning content into the base's REASONING_* + legacy STEP_STARTED/STEP_FINISHED lifecycle), andtransformStructuredOutput(subclasses can opt out of the default null→undefined transform). Defaults preserve existing behaviour for ai-openai / ai-grok.@tanstack/ai-groq— rewritten as a thin subclass (~91 LOC, down from 650). Dropsgroq-sdkin favour of the OpenAI SDK pointed athttps://api.groq.com/openai/v1(the same pattern as ai-grok against xAI). Preserves thex_groq.usagequirk via a small stream wrapper.@tanstack/ai-openrouter— rewritten as a subclass with hook overrides (~396 LOC, down from 807). Keeps@openrouter/sdkfor typed provider routing, plugins, and metadata; a small request shape converter (max_tokens→maxCompletionTokens, etc.) and chunk shape adapter bridge the SDK boundary. Provider routing, app attribution headers (httpReferer/appTitle), reasoning variants, andRequestAbortedErrorhandling are preserved.Net: −731 lines, ~1k LOC of duplicated stream/tool/lifecycle code removed. Unblocks #527's centralised structured-output lift for groq + openrouter.
Why ai-ollama is out of scope
Ollama's native API (
ollamanpm package) uses a different wire format from OpenAI Chat Completions — different request shape (options: { num_ctx, num_gpu, ... }), different chunk shape (chunk.message.{content, tool_calls, thinking},chunk.done), non-incremental tool-call streaming, and a differentformatfield for structured output. The base'sprocessStreamChunks(the bulk of the duplication win) assumes OpenAI Chat Completions chunks; bridging Ollama would require overriding every inherited method, leaving the base doing no useful work. The earlier changeset (refactor-providers-to-shared-packages.md) already documented this.Two notable test-contract changes flagged in the changeset:
ai-openrouterstructuredOutputerror wrapping ("Structured output generation failed: ..."and"Structured output response contained no content") is replaced by the shared base's cleaner unwrapped errors. Two test assertions updated to match.transformStructuredOutputto the identity function.Test plan
pnpm --filter @tanstack/openai-base test:types test:lib test:eslint test:build— 71/71 unit, types/lint/build cleanpnpm --filter @tanstack/ai-groq test:types test:lib test:eslint test:build— 17/17 unit, types/lint/build cleanpnpm --filter @tanstack/ai-openrouter test:types test:lib test:eslint test:build— 43/43 unit, types/lint/build cleanpnpm --filter @tanstack/ai-openai test:lib— 131/131 (regression check on the base hooks)pnpm --filter @tanstack/ai-grok test:lib— 53/53 (regression check on the base hooks)pnpm test:types(Nx affected) — all 32 projects cleanpnpm test:eslint(Nx affected) — all 31 projects cleanpnpm --filter @tanstack/ai-e2e test:e2e -- --grep "groq"— gating signal per CLAUDE.mdpnpm --filter @tanstack/ai-e2e test:e2e -- --grep "openrouter"— gating signal per CLAUDE.mdx_groq.usageoverride)provider: { order: [...] }(validates provider routing through the request shape conversion):thinkingvariant (validatesextractReasoninghook)RequestAbortedError→ RUN_ERROR mapping)🤖 Generated with Claude Code