Commit 0f17a38
refactor(openai-base): rename, adopt openai SDK, decouple ai-openrouter (TanStack#545)
* refactor: migrate ai-groq + ai-openrouter onto @tanstack/openai-base (TanStack#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>
* ci: apply automated fixes
* fix(openai-base, ai-openrouter, ai): silent failures in chat-completions migration
Addresses regressions and pre-existing silent failures surfaced by reviewing TanStack#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>
* feat(ai-openrouter, openai-base): OpenRouter Responses (beta) adapter
Adds OpenRouterResponsesTextAdapter on top of @tanstack/openai-base's
responses-text base, mirroring the chat-completions migration in TanStack#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>
* chore(ai-groq): remove dead unused message-param types
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>
* fix(ai-openrouter): pass UNKNOWN-fallback events through verbatim
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>
* refactor(adapters): remove asChunk casts, enforce satisfies StreamChunk
Replaces ~200 sites of `asChunk({ type: 'X', ... })` (a `Record<string,
unknown> as unknown as StreamChunk` cast) with `({ type: EventType.X, ... })
satisfies StreamChunk` so the type system validates AG-UI event shape at
every emission. The cast was bypassing TypeScript's string-enum nominal
typing and masking a cluster of spec deviations now fixed:
- RUN_STARTED / RUN_FINISHED in openai-base (chat-completions + responses)
and all three summarize adapters were missing the AG-UI-required
`threadId`. Threading `options.threadId ?? generateId(this.name)` through
`aguiState` (matching the existing Gemini/Anthropic pattern) fixes it.
- RUN_ERROR emissions carried a non-existent `runId` field and the
deprecated nested `error: { message, code }` form instead of AG-UI's
top-level `message`/`code`. Both forms now coexist (deprecated kept for
back-compat) and `runId` is dropped — verified no consumer reads it
(chat-client.ts:404 only reads runId on RUN_FINISHED).
- STEP_STARTED / STEP_FINISHED in responses-text.ts were passing only the
deprecated `stepId` alias; AG-UI requires `stepName`. Now passes both.
- `finishReason` in chat-completions-text.ts was typed as `string`,
dropping below the AG-UI vocabulary. Widened `RunFinishedEvent.finishReason`
in `@tanstack/ai` to include OpenAI's `'function_call'` so it narrows
cleanly. responses-text.ts maps Responses-API `'max_output_tokens'` →
`'length'` and passes `'content_filter'` through.
- Per-event timestamps. AG-UI spec: "Optional timestamp indicating when
the event was created." Previously a single `const timestamp = Date.now()`
was captured at run start and reused on every emission across the eight
adapters; each chunk now uses `Date.now()` inline.
`@tanstack/ai/tests/test-utils.ts` `ev.*` builders are typed to return
precise event members via `satisfies StreamChunk`; the loose `chunk(type,
fields)` factory is preserved as a documented escape hatch for tests that
deliberately construct off-spec fixtures. ai-client tests no longer declare
a local `asChunk`. ai-groq's `processStreamChunks` override signature is
updated to include the new `threadId` field on `aguiState`.
Out of scope, flagged for follow-up:
- Framework tests (ai-react / ai-svelte / ai-vue) with inline string-literal
chunk arrays — their test directories aren't currently type-checked, so
they compile despite being off-spec.
- Summarize adapters omit TEXT_MESSAGE_START / TEXT_MESSAGE_END around
content emissions (separate AG-UI lifecycle gap).
Verified: pnpm -r test:types, test:lib, test:eslint, test:build all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai-openrouter): preserve assistant/tool message content fidelity
The chat adapter's convertMessage JSON-stringified Array<ContentPart>
assistant content (so a multi-part assistant turn would round-trip as
the literal JSON of the parts instead of joined text) and emitted
`content: undefined` for tool-call-only assistants where the OpenAI
Chat Completions contract documents `null`. Use the base's
extractTextContent + emit `null` for the tool-call-only case so the
override matches the chat-completions base.
The Responses adapter's convertMessagesToInput tool branch had the
same shape — JSON.stringify(message.content) fed the raw ContentPart
shape into function_call_output.output for structured tool results.
Use extractTextContent there too.
Regression tests assert (a) array-shaped assistant content extracts to
joined text rather than JSON, and (b) tool-call-only assistant content
emits `null` rather than `undefined`.
* fix(ai-groq): correct ChatCompletionNamedToolChoice shape
The interface declared a single capitalized `Function` key with no
`type` discriminator. The OpenAI / Groq Chat Completions wire format
for a named tool_choice is `{ type: 'function', function: { name } }`.
Construct a literal against the old type and the SDK's Zod schema
would either reject it or treat tool_choice as unset.
No production code constructs this type literally yet — only the
`ChatCompletionToolChoiceOption` union in the same file uses it — so
fixing the shape now is a no-op at runtime but locks the type to the
correct contract going forward.
* test(ai-groq): reset pendingMockCreate between tests
The module-level pendingMockCreate is only cleared inside
applyPendingMock when a factory call consumes it. Tests in the first
describe block instantiate the adapter without calling
setupMockSdkClient first, so a leaked value from a prior test would
inject a stale mock into a later adapter. Reset in beforeEach for
deterministic ordering regardless of test-runner permutation.
* test(e2e): route OpenRouter summarize through createOpenRouterSummarize
The feature-support matrix advertises summarize / summarize-stream for
both `openrouter` and `openrouter-responses`, but the factories
silently substituted `createOpenaiSummarize` against the OpenAI base
URL — exercising the OpenAI adapter while reporting OpenRouter
coverage. Wire `createOpenRouterSummarize` (a thin wrapper over the
OpenRouter chat adapter, used for both rows since the summarize
endpoint is chat-completions-only) against the LLMOCK base so the
matrix's claim is actually verified.
* chore(ai-openrouter): declare zod as peer dependency
Sibling adapters (`ai-openai`, `ai-groq`, `ai-grok`) all declare zod
as a peerDependency so a consumer that passes a Zod tool schema gets
a single zod instance shared with this adapter. Without the peerDep,
strict installs (pnpm `strict-peer-dependencies`, yarn berry pnp) can
end up with two zod copies — one transitive via `@openrouter/sdk` or
`@tanstack/ai`, one direct — and `instanceof ZodType` checks then
fail across the boundary.
* fix(ai-groq): drop spurious timestamp field from processStreamChunks override
The Groq subclass declared its aguiState parameter with an extra
`timestamp: number` field that does not exist on the base class's
aguiState type. TypeScript's bivariant method-parameter checks let
the wider type pass typecheck, but at runtime the body never reads
`timestamp` and the field is never populated by the base, so any
caller (or future override) that relied on the declared shape would
observe `undefined`. Realign the override's parameter type with the
base.
* fix(ai-openrouter): stringify error.code on response.failed events
The chunk-level 'error' branch in adaptOpenRouterResponsesStreamEvents
already stringifies provider codes so they survive
toRunErrorPayload's string-only code filter, but the parallel
response.failed / response.incomplete path went through
toSnakeResponseResult which forwarded `r.error.code` raw. A provider
that returned a numeric code (401/429/500/…) on a terminal failure
event would lose it on the way through to RUN_ERROR.
Mirror the chunk-level stringification inside toSnakeResponseResult
and add a regression test for response.failed with a numeric
error.code.
* fix(ai-openrouter): default image data URI mime type to octet-stream
When a base64 image source has no mimeType the override produced a
literal `data:undefined;base64,...` URI that the upstream rejects as
invalid. The chat-completions base defaults to
`application/octet-stream` for exactly this case; mirror the same
defaulting in the OpenRouter convertContentPart override. Regression
test asserts the data URI no longer contains the literal `undefined`.
* fix(openai-base): stop processing chunks after top-level error event
The Responses adapter's processStreamChunks marked `runFinishedEmitted`
on a top-level chunk.type === 'error' to prevent the synthetic
terminal block from firing, but it did not return from the for-await
loop. Any subsequent chunks the upstream delivered after a terminal
error event (a stray output_text.delta, an output_item.done, etc.)
would continue to emit lifecycle events past RUN_ERROR, violating the
'RUN_ERROR is terminal' contract.
Mirror the response.failed / response.incomplete branches above:
return after yielding RUN_ERROR. Regression test covers the case
where the upstream continues delivering chunks after a top-level
error event and asserts no further chunks reach the consumer.
* fix(openai-base, ai-openrouter): route Responses structuredOutput through transformStructuredOutput hook
The Responses base hard-coded transformNullsToUndefined on parsed
structured-output JSON, leaving no hook for subclasses to opt out.
The changeset's promise of 'transformStructuredOutput for subclasses
(like OpenRouter) that preserve nulls in structured output instead
of converting them to undefined' was therefore only fulfilled on
the chat-completions surface — the matching Responses adapter would
silently strip nulls regardless of provider intent.
Add the transformStructuredOutput protected hook on
OpenAICompatibleResponsesTextAdapter mirroring the chat-completions
base's design, and override it as a no-op on
OpenRouterResponsesTextAdapter so OpenRouter callers see null
sentinels round-trip identically across the two adapter surfaces.
Regression test asserts a structuredOutput response containing
`nickname: null` round-trips as null (not undefined) through the
Responses adapter.
* fix(ai-openrouter): extract text from array-shaped tool message content
The chat-completions adapter's convertMessage tool branch still
JSON-stringified Array<ContentPart> tool message content, so a tool
result delivered as structured parts (e.g. [{type:'text', content:
'"temp":'}, {type:'text', content:'72'}]) reached the model as the
literal JSON of the parts rather than the joined textual result. The
parallel responses adapter override was fixed earlier; this mirrors
the same fix on the chat-completions path so both surfaces handle
structured tool content identically.
Regression test feeds a structured tool result and asserts the wire
payload's tool message content is the joined text without any
'"type":"text"' leakage.
* chore(ai-groq): declare @tanstack/ai as workspace devDependency
Every sibling adapter (ai-openai, ai-grok, ai-openrouter, ai-anthropic,
ai-gemini, ai-fal, ai-ollama) explicitly lists `@tanstack/ai:
workspace:*` under devDependencies in addition to declaring it as a
peer. ai-groq omitted the devDep entry, so resolution worked only via
pnpm's autoInstallPeers behaviour — toggling that off (strict installs,
some yarn berry configs) would silently break ai-groq while every
other adapter kept working. Add the dev dep for parity.
* fix(ai-openrouter): route audio URLs to text fallback on chat-completions
The chat-completions OpenRouter adapter's convertContentPart for audio
unconditionally emitted `{ type: 'input_audio', inputAudio: { data,
format: 'mp3' } }` — but `data` is supposed to be base64. A
URL-sourced audio part therefore shipped the literal URL string into
the base64 slot, which the upstream rejects (or worse, treats as
garbage audio bytes). The Responses adapter already handles this by
routing URL audio through `input_file` (where the URL belongs);
chat-completions has no `input_file` shape on this surface, so
mirror the existing document fallback: emit a text reference noting
the URL. Callers needing real audio URL support should use the
Responses adapter.
* docs(ai-groq): correct message-types header — Groq SDK was dropped
The header comment claimed these types "mirror the Groq SDK types",
but the migration dropped the groq-sdk dependency entirely in favour
of pointing the OpenAI SDK at Groq's /openai/v1 base URL. The file is
now the source of truth for Groq-specific wire fields (compound
tools, citation/service-tier provider options, …), not a mirror of an
external SDK. Update the header to reflect the post-migration role.
* fix(ai-openrouter): reject inline document data on chat-completions
The chat-completions convertContentPart 'document' branch unconditionally
returned `{ type: 'text', text: `[Document: ${part.source.value}]` }`.
For URL sources that's a reasonable degradation. For data sources,
`part.source.value` is the raw base64 payload — a multi-megabyte
document would be inlined into the prompt verbatim, blowing the
context window and leaking the document content as plaintext bytes.
Branch on `part.source.type`: URL sources keep the text-reference
fallback, data sources throw with a clear error pointing the caller at
the Responses adapter (which has proper `input_file` support for
inline document data). Mirrors the audio URL/data branching added in
the prior round.
* refactor: rename @tanstack/openai-base → @tanstack/openai-compatible
The "base" name implied this package tracked OpenAI's product roadmap. In reality it implements two OpenAI-shaped wire-format protocols (Chat Completions, Responses) that multiple providers ship — OpenRouter, Groq, Grok, vLLM, SGLang, Together. "OpenAI-compatible" is the industry term for this family (cf. Vercel's @ai-sdk/openai-compatible, LiteLLM, BentoML, Lightning AI).
OpenRouter's beta Responses endpoint routes to Claude, Gemini, and other underlying models, confirming that /v1/responses (like /v1/chat/completions) is a multi-provider wire format rather than OpenAI-only — so the Responses adapter stays alongside Chat Completions in the renamed package.
Pure rename, no behavior change. Class names (OpenAICompatibleChatCompletionsTextAdapter, OpenAICompatibleResponsesTextAdapter, …) and protected hook contracts are unchanged. Consumer packages (ai-openai, ai-openrouter, ai-groq, ai-grok) only update internal import paths; public API is unchanged.
@tanstack/openai-base@0.2.x remains published on npm for any pinned lockfile references but will receive no further updates. A README in the renamed package documents the protocol-vs-product contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* refactor: rename @tanstack/openai-compatible → @tanstack/ai-openai-compatible
Match the `ai-*` prefix convention used by every other package in the AI subnamespace (ai-utils, ai-openai, ai-anthropic, ai-client, ai-react, …). `@tanstack/` is a flat namespace shared across all TanStack products (Query, Router, Table, Form, AI, …), so `@tanstack/openai-compatible` alone gives no signal about which TanStack product it belongs to.
Pure rename of the rename in 06d3d8c; no behavior change. Directory `packages/typescript/openai-compatible` → `packages/typescript/ai-openai-compatible`, package.json `name` field, consumer dependency declarations, TypeScript imports, README, CHANGELOG header, both changesets all updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ai-openai-compatible, ai-openrouter): explain the protocol-vs-product framing
Rewrite the `@tanstack/ai-openai-compatible` README to lead with the thinking — OpenAI authored two wire formats (Chat Completions and Responses) that many vendors implement, so the package contains the shared logic for talking to any server that speaks one of those wire formats, not "the base for OpenAI." Add a side-by-side table for "what goes here vs in @tanstack/ai-openai" and a contributor rule of thumb ("a field belongs here only if at least two compatible providers support it").
Expand the leading docstrings on both OpenRouter text adapters to explicitly answer "why does this extend from @tanstack/ai-openai-compatible?" — OpenRouter implements OpenAI's wire formats verbatim (Chat Completions natively, Responses as a beta routing layer that fans out to Claude/Gemini/etc.), so the shared base lets us inherit ~1k LOC of stream accumulation, partial-JSON buffering, AG-UI lifecycle, and structured-output coercion rather than duplicating it.
No code change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* docs(adapters/openrouter): add Chat Completions vs Responses (beta) section
The OpenRouter adapter package now ships two adapters (`openRouterText` for /v1/chat/completions and `openRouterResponsesText` for /v1/responses beta). Document the difference for consumers: both route to any underlying model in the catalogue (Anthropic, Google, Meta, …); the wire format describes the client → OpenRouter call, not which provider answers. Add a side-by-side table, a basic example for the Responses adapter, and beta caveats (no branded server-tools yet; prefer the chat-completions adapter if in doubt).
No mention of the internal shared-base package — that's an implementation detail consumers don't need to track.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(ai-openai-compatible): narrow to chat/responses; decouple from openai SDK
Make `OpenAICompatibleChatCompletionsTextAdapter` and `OpenAICompatibleResponsesTextAdapter`
abstract. Subclasses now own SDK client construction and implement the
`callChatCompletion*` / `callResponse*` hooks. The base never imports `openai`
at runtime — only as types — so `dist/esm/*.js` is openai-free and the package's
`openai` dep moves to optional `peerDependencies` + `devDependencies`.
Delete image/tts/transcription/video bases (single-user; only ai-openai extended
them, so they're now standalone classes there). Move summarize to `@tanstack/ai`
core as `ChatStreamSummarizeAdapter` — it's protocol-agnostic, wraps any
`ChatStreamCapable`. Provider-specific `*SummarizeAdapter` classes deleted,
replaced by thin factory functions returning `ChatStreamSummarizeAdapter` directly.
ai-grok duplicates its image adapter standalone (~150 LOC; shared base wasn't
worth the indirection for thin SDK wrappers).
Also fix the round-3 double `TOOL_CALL_END` regression in the Responses adapter:
`function_call_arguments.done` now gates on `!metadata.ended` so the
output_item.done backfill path can't emit a duplicate close. Regression test added.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* refactor(ai): rename chat-stream-wrapper to chat-stream-summarize
* refactor(summarize): unify provider summarize adapters on chat-stream wrapper
- Migrate anthropic, gemini, ollama, openrouter summarize adapters to thin
factories over ChatStreamSummarizeAdapter, matching the openai/grok pattern.
Drops ~600 lines of duplicated streaming/error/usage handling.
- Thread modelOptions from SummarizationOptions through the activity layer
and into the wrapped text adapter's chatStream so provider-specific knobs
(cache control, plugins, safety settings, tuning params) reach the wire.
- Add InferTextProviderOptions<TAdapter> helper to extract per-model
provider options from a text adapter's ~types.
- Drop bespoke XSummarizeProviderOptions interfaces from all 6 providers;
provider summarize types now resolve to the text adapter's per-model
options shape, giving accurate IntelliSense for modelOptions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* refactor(ai-openai-compatible): vendor wire types; drop openai dep
Move the OpenAI wire-format types from `import type ... from 'openai'`
to local `src/types/{chat-completions,responses,tools}.ts` — hand-written
minimal interfaces covering only the fields the base reads/writes.
- Drop `openai` from `@tanstack/ai-openai-compatible`'s peerDependencies,
peerDependenciesMeta, and devDependencies.
- Drop `openai` from `@tanstack/ai-openrouter`'s devDependencies (it was
only there to satisfy type leakage from `@tanstack/ai-openai-compatible`).
- Update `@tanstack/ai-openai`'s text adapter overrides to use the local
protocol types and cast at the SDK boundary, keeping variance compat
with the now-local base.
Net effect: `@tanstack/ai-openai-compatible`'s emitted .d.ts contains
zero `from 'openai'` references. End-users installing
`@tanstack/ai-openrouter` (or any future protocol-compatible adapter
that doesn't itself use the openai SDK at runtime) no longer pull
`openai` into their dependency tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* refactor(openai-base): rename, adopt openai SDK, decouple ai-openrouter
- Rename @tanstack/ai-openai-compatible → @tanstack/openai-base. Vendored
wire types deleted; the base now imports from openai/resources/* directly.
- Drop the abstract callChatCompletion* / callResponse* hooks. Base takes a
pre-built OpenAI client in its constructor and calls the SDK itself;
subclasses (ai-openai, ai-grok, ai-groq) just pass a configured client.
- Decouple @tanstack/ai-openrouter from openai-base entirely. The two adapters
now extend BaseTextAdapter directly, inline their stream processors, and
read OpenRouter's camelCase types natively — dropping ~300 LOC of
snake↔camel reshaping that existed only to satisfy the inherited base.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* Corrected package versions
* refactor(adapters): address PR review — jsdoc, casts, zod, finishReason
Apply review feedback from PR TanStack#545:
- Restore JSDoc removed during the openai-base media/summarize refactor
(26 blocks across ai-openai, ai-grok, ai-anthropic, ai-gemini,
ai-openrouter adapters). Only restore where the documented symbol still
exists post-refactor; skip JSDoc tied to removed classes / provider-
options interfaces.
- Drop `as` casts on stream chunks in ai-openrouter (responses-text.ts
output_item.{added,done} handlers, response.completed handler) by typing
`NormalizedStreamEvent.item` as the SDK's `OutputItems` discriminated
union and `.response` as `Partial<OpenResponsesResult>`. Discriminated-
union narrowing now works without bypass.
- Drop request-builder casts in ai-openrouter/{text,responses-text}.ts:
`as InputsItem`, `as ChatMessages`, `as ChatContentItems`,
`as ResponsesRequest['tools' | 'text' | 'input']`,
`as Omit<ChatRequest, 'stream'>`, `as Record<string, any>` on
modelOptions spread.
- Drop SDK-return casts `as AsyncIterable<StreamEvents>` /
`as AsyncIterable<ChatStreamChunk>` — `EventStream<T>` already is
`AsyncIterable<T>`.
- Drop `tool as Tool` in the webSearchTool guard — `Tool<any, any, any>`
is assignable to `Tool` directly.
- Remove `'function_call'` from RunFinishedEvent.finishReason union.
Normalize OpenAI's legacy v1 function_call termination to `tool_calls`
inside chat-completions-text — the SDK-vocabulary value no longer leaks
into the public AG-UI type.
- Drop redundant `satisfies StreamChunk` from yield/array-element sites
across adapters and ai-client tests. The contextual type from
`AsyncIterable<StreamChunk>` / `Array<StreamChunk>` already validates
every emission; the suffix added no extra safety.
- Annotate the `ev.*` builders in ai/tests/test-utils.ts with explicit
return types (RunStartedEvent, TextMessageStartEvent, …) instead of
`satisfies StreamChunk`. Each builder now returns the precise event
variant rather than the wide union.
- Drop zod from ai-openrouter peerDependencies — no source imports zod;
it's only used in tests, where it stays as a devDep. (OpenRouter SDK
already declares zod as a regular dep, so transitive consumers aren't
affected.)
- Clean up mid-PR rename leftovers: stale "openai-compatible adapters"
jsdoc in ai-openai/utils/client.ts, and `'openai-compatible'` /
`'openai-compatible-responses'` default-name strings in the
openai-base test subclasses (now `openai-base` / `openai-base-responses`).
* refactor(ai-openrouter): drop residual chunk casts in responses-text
Extend the no-`as`-on-chunks principle (PR TanStack#545 review) to five sibling
sites missed by 44db925:
- `response.created/in_progress/incomplete/failed` model + error/incomplete
capture (lines 462, 491): `NormalizedStreamEvent.response` is already
`Partial<OpenResponsesResult>`, so the duck-type casts were redundant.
Read `chunk.response?.{model,error,incompleteDetails}` directly.
- `response.content_part.{added,done}` (lines 629, 673): type
`NormalizedStreamEvent.part` as the SDK's `ContentPartAddedEventPart`
discriminated union (`ResponseOutputText | ReasoningTextContent |
OpenAIResponsesRefusalContent | Unknown<'type'>`) and switch
`handleContentPart` to narrow on `part.type`. The previous `text?` /
`refusal?` duck-type allowed unsafe access on unknown parts.
- `response.completed` `outputItems.some(item.type === 'function_call')`
(line 998): the array element type is already `OutputItems`, line 921
above already narrows without a cast — leftover.
Behaviourally identical; verified by openrouter unit tests (80/80) and
e2e suite (30/30).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* refactor(ai): tighten summarize TProviderOptions to Record<string, unknown>
Unify the generic constraint and default across the summarize surface:
- `SummarizationOptions`: `extends object = Record<string, any>` →
`extends Record<string, unknown> = Record<string, unknown>`
- `SummarizeAdapter` / `BaseSummarizeAdapter`: constraint tightened from
`extends object` to `extends Record<string, unknown>` (default was
already `Record<string, unknown>`)
- `ChatStreamSummarizeAdapter`: `extends object = Record<string, any>` →
`extends Record<string, unknown> = Record<string, unknown>`
- `activities/summarize/index.ts` instantiation sites: literal
`<string, object>` → `<string, Record<string, unknown>>`
Removes the three-way default split (`object` / `Record<string, any>` /
`Record<string, unknown>`) that lived inside the summarize folder, and
forces unparameterised consumers to narrow before indexed access.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* docs(openai-base): rewrite README; consolidate summarize changeset
README:
- Drop the broken "Renamed from" note (referenced an outdated state).
- Drop the Vercel `@ai-sdk/openai-compatible` industry-term paragraph and
the surrounding "Why this package exists" rationale that explained the
prior rename — package is back to `openai-base`, that history is moot.
- Reframe TL;DR around the actual current contract: "providers that drive
the official `openai` SDK against a different `baseURL`" (only
ai-openai, ai-grok, ai-groq remain on the base after this PR).
- Remove ai-openrouter from subclass lists and the architecture diagram —
it was decoupled in this PR and now extends `BaseTextAdapter` directly.
- Rewrite the hooks section: the old `callChatCompletion(Stream)` /
`callResponse(Stream)` abstract methods were removed in 7aff8b1; the
base now takes a pre-built `OpenAI` client and calls
`client.chat.completions.create` / `client.responses.create` itself.
Document `convertMessage`, `mapOptionsToRequest`, `extractReasoning`,
`transformStructuredOutput`, `makeStructuredOutputCompatible`,
`processStreamChunks`, `extractTextFromResponse` as the real surface.
- Update "build a new provider" example to point at ai-grok / ai-groq.
Changesets:
- Replace the narrow `summarize-tighten-provider-options-generic.md`
(which only covered 6d99fad) with a comprehensive
`summarize-unify-on-chat-stream-wrapper.md` that also covers e0dcb77
(provider summarize unification on `ChatStreamSummarizeAdapter`,
`modelOptions` plumbing fix in the activity layer, new
`InferTextProviderOptions<TAdapter>` helper, and removal of the
bespoke `*SummarizeProviderOptions` interfaces from 6 provider
packages). Adds patch bumps for ai-anthropic / ai-gemini / ai-ollama
which were previously uncovered.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): revert summarize TProviderOptions constraint to extends object
6d99fad tightened the constraint from `extends object` to `extends
Record<string, unknown>` alongside aligning the default. The default
change was correct; the constraint change broke vite build / DTS emit
for ai-openai, ai-anthropic, ai-gemini, ai-grok, ai-ollama. Their
summarize factories instantiate `ChatStreamSummarizeAdapter<TModel,
InferTextProviderOptions<XTextAdapter<TModel>>>`, and the inferred
per-model option shapes (`OpenAIBaseOptions & OpenAIReasoningOptions &
...` etc.) are typed interfaces with named optional fields and no
string index signature — TS won't assign them to
`Record<string, unknown>`.
Revert just the constraint to `extends object`, keep the default at
`Record<string, unknown>`. Restores the pattern `BaseSummarizeAdapter`
already had on main, now applied uniformly across all four
declarations. The 7 activity-layer `<string, Record<string, unknown>>`
instantiations in summarize/index.ts revert to `<string, object>`, and
the two `summarizeOptions: SummarizationOptions = {...}` literals are
explicitly annotated `SummarizationOptions<object>` so the
modelOptions: object | undefined destructured from the activity-layer
options assigns correctly.
Changeset paragraph 5 amended to describe what actually shipped
(default-aligned, constraint preserved).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* refactor(ai): drop the SummarizationOptions<object> annotation noise
Let TS infer summarizeOptions from the literal in runSummarize /
runStreamingSummarize. The contextual check happens at the
adapter.summarize(...) / adapter.summarizeStream(...) call site against
the adapter's own typed signature, which is sufficient — the explicit
local annotation was just visual noise. Drops the unused
SummarizationOptions import too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai, ai-client): replace removed StreamChunk casts with typed event data
Follow-up to the cast removals: where the old `as unknown as StreamChunk`
casts were hiding real data-shape issues, fix the data instead of
re-introducing the bypass.
Source:
- ai-client/src/connection-adapters.ts: synth RUN_FINISHED chunk now
includes `threadId` (the cast had been hiding the missing required
field). Use `EventType.RUN_FINISHED` / `EventType.RUN_ERROR` literals.
Test helpers (`chunk()` / `makeChunk()` / `sc()`):
- Replace string-typed `(type: string, fields) => StreamChunk` (which
needed `as unknown as StreamChunk` to lie) with a generic
`<T extends StreamChunk['type']>(type: T, fields?) =>
Extract<StreamChunk, { type: T }>`. One typed cast remains inside
each helper at the boundary; no `as unknown` casts.
- `sc()` retyped as a typed identity (`<T extends StreamChunk>(c: T) => T`)
so inline literal narrowing flows from the `type` discriminant.
Inline literals + missing fields fixed at call sites:
- All `chunk('X', ...)` → `chunk(EventType.X, ...)` across
stream-processor.test.ts (42), strip-to-spec-middleware.test.ts (4),
chat.test.ts (1).
- All `type: 'X'` inside test object literals → `type: EventType.X`
across stream-to-response, custom-events-integration, extend-adapter,
stream-processor (the four MESSAGES_SNAPSHOT inline literals).
- extend-adapter mock RUN_FINISHED gained `threadId`.
- custom-events-integration TOOL_CALL_START gained `toolCallName`
(the cast had been hiding the missing required field).
- stream-processor MESSAGES_SNAPSHOT bodies (the two whose casts were
removed) converted from TanStack `UIMessage` shape (parts/createdAt)
to AG-UI `Message` shape (id/role/content) — the processor casts
internally, but the upstream MessagesSnapshotEvent.messages field
requires AG-UI Message.
types.ts is untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Alem Tuzlak <t.zlak@hotmail.com>1 parent a4ddbbf commit 0f17a38
103 files changed
Lines changed: 8121 additions & 5227 deletions
File tree
- .changeset
- docs/adapters
- packages/typescript
- ai-anthropic/src
- adapters
- ai-client
- src
- tests
- ai-gemini
- src
- adapters
- tests
- ai-grok
- src
- adapters
- utils
- ai-groq
- src
- adapters
- text
- utils
- tests
- ai-ollama/src
- adapters
- ai-openai/src
- adapters
- text
- utils
- ai-openrouter
- src
- adapters
- internal
- text
- tests
- ai
- src
- activities
- summarize
- tests
- openai-base
- src
- adapters
- tools
- types
- utils
- tests
- testing/e2e
- src
- lib
- routes
- tests
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
48 | 49 | | |
49 | 50 | | |
50 | 51 | | |
| |||
122 | 123 | | |
123 | 124 | | |
124 | 125 | | |
125 | | - | |
126 | | - | |
| 126 | + | |
| 127 | + | |
127 | 128 | | |
128 | 129 | | |
129 | 130 | | |
130 | 131 | | |
131 | 132 | | |
132 | | - | |
133 | 133 | | |
134 | 134 | | |
135 | 135 | | |
136 | | - | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
137 | 172 | | |
138 | 173 | | |
139 | 174 | | |
| |||
0 commit comments