Skip to content

refactor: migrate ai-groq + ai-openrouter onto @tanstack/openai-base (#543)#545

Draft
tombeckenham wants to merge 6 commits into
mainfrom
543-migrate-ai-groq-ai-openrouter-ai-ollama-to-openai-base-+-parameterize-the-base-for-sdk-shape-variance
Draft

refactor: migrate ai-groq + ai-openrouter onto @tanstack/openai-base (#543)#545
tombeckenham wants to merge 6 commits into
mainfrom
543-migrate-ai-groq-ai-openrouter-ai-ollama-to-openai-base-+-parameterize-the-base-for-sdk-shape-variance

Conversation

@tombeckenham
Copy link
Copy Markdown
Contributor

Summary

Closes #543 (for groq + openrouter; ollama stays on BaseTextAdapter — see rationale below).

  • @tanstack/openai-base — adds four protected hooks on OpenAICompatibleChatCompletionsTextAdapter so 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), and transformStructuredOutput (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). Drops groq-sdk in favour of the OpenAI SDK pointed at https://api.groq.com/openai/v1 (the same pattern as ai-grok against xAI). Preserves the x_groq.usage quirk via a small stream wrapper.
  • @tanstack/ai-openrouter — rewritten as a subclass with hook overrides (~396 LOC, down from 807). Keeps @openrouter/sdk for typed provider routing, plugins, and metadata; a small request shape converter (max_tokensmaxCompletionTokens, etc.) and chunk shape adapter bridge the SDK boundary. Provider routing, app attribution headers (httpReferer / appTitle), reasoning variants, and RequestAbortedError handling 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 (ollama npm 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 different format field for structured output. The base's processStreamChunks (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:

  1. ai-openrouter structuredOutput error 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.
  2. ai-openrouter previously preserved nulls in structured-output results; preserved via overriding transformStructuredOutput to the identity function.

Test plan

  • pnpm --filter @tanstack/openai-base test:types test:lib test:eslint test:build — 71/71 unit, types/lint/build clean
  • pnpm --filter @tanstack/ai-groq test:types test:lib test:eslint test:build — 17/17 unit, types/lint/build clean
  • pnpm --filter @tanstack/ai-openrouter test:types test:lib test:eslint test:build — 43/43 unit, types/lint/build clean
  • pnpm --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 clean
  • pnpm test:eslint (Nx affected) — all 31 projects clean
  • pnpm --filter @tanstack/ai-e2e test:e2e -- --grep "groq" — gating signal per CLAUDE.md
  • pnpm --filter @tanstack/ai-e2e test:e2e -- --grep "openrouter" — gating signal per CLAUDE.md
  • Manual smoke: real Groq call with a tool (validates x_groq.usage override)
  • Manual smoke: real OpenRouter call with provider: { order: [...] } (validates provider routing through the request shape conversion)
  • Manual smoke: real OpenRouter call with :thinking variant (validates extractReasoning hook)
  • Manual smoke: real OpenRouter call cancelled mid-stream (validates RequestAbortedError → RUN_ERROR mapping)

🤖 Generated with Claude Code

…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>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a4787c3b-83ad-4d5e-887a-19fd21d54af1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 543-migrate-ai-groq-ai-openrouter-ai-ollama-to-openai-base-+-parameterize-the-base-for-sdk-shape-variance

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 11, 2026

🚀 Changeset Version Preview

4 package(s) bumped directly, 24 bumped as dependents.

🟨 Minor bumps

Package Version Reason
@tanstack/openai-base 0.2.1 → 0.3.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai 0.16.0 → 0.16.1 Changeset
@tanstack/ai-groq 0.1.11 → 0.1.12 Changeset
@tanstack/ai-openrouter 0.8.5 → 0.8.6 Changeset
@tanstack/ai-client 0.9.1 → 0.9.2 Dependent
@tanstack/ai-code-mode 0.1.10 → 0.1.11 Dependent
@tanstack/ai-code-mode-models-eval 0.0.15 → 0.0.16 Dependent
@tanstack/ai-code-mode-skills 0.1.10 → 0.1.11 Dependent
@tanstack/ai-devtools-core 0.3.27 → 0.3.28 Dependent
@tanstack/ai-event-client 0.3.0 → 0.3.1 Dependent
@tanstack/ai-fal 0.7.3 → 0.7.4 Dependent
@tanstack/ai-grok 0.7.3 → 0.7.4 Dependent
@tanstack/ai-isolate-cloudflare 0.2.1 → 0.2.2 Dependent
@tanstack/ai-isolate-node 0.1.10 → 0.1.11 Dependent
@tanstack/ai-isolate-quickjs 0.1.10 → 0.1.11 Dependent
@tanstack/ai-openai 0.8.5 → 0.8.6 Dependent
@tanstack/ai-preact 0.6.22 → 0.6.23 Dependent
@tanstack/ai-react 0.8.2 → 0.8.3 Dependent
@tanstack/ai-solid 0.7.2 → 0.7.3 Dependent
@tanstack/ai-svelte 0.7.2 → 0.7.3 Dependent
@tanstack/ai-vue 0.7.2 → 0.7.3 Dependent
@tanstack/ai-vue-ui 0.1.33 → 0.1.34 Dependent
@tanstack/preact-ai-devtools 0.1.31 → 0.1.32 Dependent
@tanstack/react-ai-devtools 0.2.31 → 0.2.32 Dependent
@tanstack/solid-ai-devtools 0.2.31 → 0.2.32 Dependent
ts-svelte-chat 0.1.41 → 0.1.42 Dependent
ts-vue-chat 0.1.41 → 0.1.42 Dependent
vanilla-chat 0.0.37 → 0.0.38 Dependent

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 11, 2026

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit 2c52bd1

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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 11, 2026

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@545

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@545

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@545

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@545

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@545

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@545

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@545

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@545

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@545

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@545

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@545

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@545

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@545

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@545

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@545

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@545

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@545

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@545

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@545

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@545

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@545

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@545

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@545

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@545

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@545

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@545

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@545

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@545

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@545

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@545

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@545

commit: 2c52bd1

tombeckenham and others added 4 commits May 12, 2026 09:23
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate ai-groq, ai-openrouter, ai-ollama to openai-base + parameterize the base for SDK shape variance

1 participant