You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Captured from PR #409 comment so the design proposal doesn't get lost regardless of how #409 lands.
Context
After #409 merges, only ai-openai and ai-grok will extend the new @tanstack/openai-base. ai-groq, ai-openrouter, and ai-ollama still extend BaseTextAdapter directly — together that's ~2k lines of duplicated text-adapter code the bases were designed to absorb.
For ai-groq the migration looks mechanical (Chat Completions wire format, OpenAI SDK works fine pointed at Groq's endpoint). ai-openrouter is the interesting one — design question worth resolving before assuming it slots in.
Wire format: identical. SDK shape: not.
OpenRouter's HTTP API is OpenAI-compatible Chat Completions, so on the wire the base's SSE accumulator, partial-JSON, tool-call buffers, and RUN_ERROR taxonomy all apply unchanged. But the TypeScript SDK (@openrouter/sdk, Speakeasy-generated) has a different shape from the OpenAI SDK the base assumes:
OpenAI SDK (base)
@openrouter/sdk
Call shape
client.chat.completions.create(params)
client.chat.send({ chatRequest: params })
Field naming
snake_case (max_completion_tokens, top_p)
camelCase (maxCompletionTokens, topP)
Abort error
APIUserAbortError
RequestAbortedError from @openrouter/sdk/models/errors
Grok gets away with a ~60-line subclass because xAI's "OpenAI-compatible" endpoint accepts the OpenAI SDK verbatim — withGrokDefaults just rewrites the baseURL. OpenRouter's SDK is not a drop-in.
OpenRouter-specific surface
These are real user-facing features, not incidental:
Response metadata — which provider served the request, which underlying model was actually used, generation ID for cost lookup.
App attribution headers — HTTP-Referer, X-Title.
Considered and ruled out: dropping @openrouter/sdk
The cleanest-looking option would be replacing @openrouter/sdk with the OpenAI SDK pointed at openrouter.ai/api/v1 (the grok approach). That deletes ~700 lines and the base works unchanged.
Ruled out: it forces provider routing through untyped extra_body pass-through and loses the typed response metadata. That's a type-safety regression on OpenRouter's headline feature, and the SDK is purpose-built around exactly that surface — keeping it is the point.
Proposal: parameterize the base with two protected hooks
Add to OpenAICompatibleChatCompletionsTextAdapter:
Default impl calls the OpenAI SDK (covers grok and any future "OpenAI-compatible-endpoint" providers like deepseek / together / fireworks). ai-openrouter overrides to call @openrouter/sdk.
Everything else — tool/message converters, SSE accumulator, partial-JSON, RUN_ERROR taxonomy, structured output, lifecycle — stays in the base. The ai-openrouter subclass collapses to ~150 lines (SDK shim + provider-routing typing + response metadata extraction) vs. 807 today.
PR #527 (streaming structured output) currently duplicates ~290 lines of structuredOutput + chatStreamStructured across openai/grok/groq. The plan there is to lift that into the bases so each wire format has one implementation. That lift only covers openrouter if openrouter is on the Chat Completions base. If #409 ships with openrouter unmigrated, #527 has to either keep duplicating into openrouter or fold the openrouter migration into a feature PR.
Captured from PR #409 comment so the design proposal doesn't get lost regardless of how #409 lands.
Context
After #409 merges, only
ai-openaiandai-grokwill extend the new@tanstack/openai-base.ai-groq,ai-openrouter, andai-ollamastill extendBaseTextAdapterdirectly — together that's ~2k lines of duplicated text-adapter code the bases were designed to absorb.For
ai-groqthe migration looks mechanical (Chat Completions wire format, OpenAI SDK works fine pointed at Groq's endpoint).ai-openrouteris the interesting one — design question worth resolving before assuming it slots in.Wire format: identical. SDK shape: not.
OpenRouter's HTTP API is OpenAI-compatible Chat Completions, so on the wire the base's SSE accumulator, partial-JSON, tool-call buffers, and RUN_ERROR taxonomy all apply unchanged. But the TypeScript SDK (
@openrouter/sdk, Speakeasy-generated) has a different shape from the OpenAI SDK the base assumes:@openrouter/sdkclient.chat.completions.create(params)client.chat.send({ chatRequest: params })max_completion_tokens,top_p)maxCompletionTokens,topP)APIUserAbortErrorRequestAbortedErrorfrom@openrouter/sdk/models/errorsGrok gets away with a ~60-line subclass because xAI's "OpenAI-compatible" endpoint accepts the OpenAI SDK verbatim —
withGrokDefaultsjust rewrites thebaseURL. OpenRouter's SDK is not a drop-in.OpenRouter-specific surface
These are real user-facing features, not incidental:
provider: { order, fallbacks, allow_fallbacks, sort, ignore },models[]fallback chain,route,transforms: ['middle-out']. OpenRouter's distinguishing feature.HTTP-Referer,X-Title.Considered and ruled out: dropping
@openrouter/sdkThe cleanest-looking option would be replacing
@openrouter/sdkwith the OpenAI SDK pointed atopenrouter.ai/api/v1(the grok approach). That deletes ~700 lines and the base works unchanged.Ruled out: it forces provider routing through untyped
extra_bodypass-through and loses the typed response metadata. That's a type-safety regression on OpenRouter's headline feature, and the SDK is purpose-built around exactly that surface — keeping it is the point.Proposal: parameterize the base with two protected hooks
Add to
OpenAICompatibleChatCompletionsTextAdapter:Default impl calls the OpenAI SDK (covers grok and any future "OpenAI-compatible-endpoint" providers like deepseek / together / fireworks).
ai-openrouteroverrides to call@openrouter/sdk.Everything else — tool/message converters, SSE accumulator, partial-JSON, RUN_ERROR taxonomy, structured output, lifecycle — stays in the base. The
ai-openroutersubclass collapses to ~150 lines (SDK shim + provider-routing typing + response metadata extraction) vs. 807 today.Why this matters for #527
PR #527 (streaming structured output) currently duplicates ~290 lines of
structuredOutput+chatStreamStructuredacross openai/grok/groq. The plan there is to lift that into the bases so each wire format has one implementation. That lift only covers openrouter if openrouter is on the Chat Completions base. If #409 ships with openrouter unmigrated, #527 has to either keep duplicating into openrouter or fold the openrouter migration into a feature PR.Open questions
ai-groq/ai-openrouter/ai-ollamaonBaseTextAdapterdeliberate (scoping feat: extract @tanstack/openai-base and @tanstack/ai-utils packages #409 to "introduce bases, migrate later") or time-boxed?ai-groq+ai-openrouter?Happy to prototype the hook signatures on a side branch if it'd help the conversation.