Skip to content

feat: extract @tanstack/openai-base and @tanstack/ai-utils packages#409

Open
AlemTuzlak wants to merge 15 commits intomainfrom
feat/extract-openai-base-and-ai-utils
Open

feat: extract @tanstack/openai-base and @tanstack/ai-utils packages#409
AlemTuzlak wants to merge 15 commits intomainfrom
feat/extract-openai-base-and-ai-utils

Conversation

@AlemTuzlak
Copy link
Copy Markdown
Contributor

@AlemTuzlak AlemTuzlak commented Mar 30, 2026

Summary

Extracts shared logic into two new packages to eliminate code duplication across all adapter packages:

  • @tanstack/ai-utils — Zero-dependency, provider-agnostic utilities: generateId, getApiKeyFromEnv, transformNullsToUndefined, ModelMeta types + defineModelMeta. Used by all 9 adapter packages.
  • @tanstack/openai-base — OpenAI-compatible adapter base classes and utilities: Chat Completions text adapter, Responses API text adapter, image/summarize/transcription/TTS/video adapters, schema converter (makeStructuredOutputCompatible), 15 tool converters, shared types. Used by ai-openai and ai-grok (full adapter inheritance) and ai-groq/ai-openrouter/ai-ollama (selective utility imports).

Key results

  • ai-openai: ~1,800 lines removed, extends OpenAICompatibleResponsesTextAdapter
  • ai-grok: ~500 lines removed, extends OpenAICompatibleChatCompletionsTextAdapter
  • ai-groq, ai-openrouter, ai-ollama: utility dedup via imports
  • ai-anthropic, ai-gemini, ai-fal, ai-elevenlabs: generateId/getApiKeyFromEnv dedup via ai-utils
  • Zero breaking changes — all public APIs remain identical
  • 103 files changed, +6,469 / -3,154 lines

New package architecture

@tanstack/ai (core)
    ↑
@tanstack/ai-utils (new, zero deps)
    ↑
@tanstack/openai-base (new, deps: ai-utils + openai SDK)
    ↑              ↑
ai-openai      ai-grok          ← extend adapter classes

Test plan

  • pnpm build — 28/28 projects pass
  • pnpm test:lib — all unit tests pass across all packages
  • pnpm test:types — all type checks pass
  • pnpm test:build — publint validates all packages
  • pnpm format — all files formatted
  • New packages have dedicated test suites (21 tests in ai-utils, 48 in openai-base)
  • Existing tests in all migrated packages pass unchanged

Summary by CodeRabbit

  • New Features

    • New shared utilities package: API-key resolution, ID generation, null→undefined transform, and model-metadata helpers with runtime validation
    • New OpenAI-compatible base: unified adapters for text (Responses & Chat), image, TTS, transcription, summarization, and video
    • Many provider tool types and converters now available from a shared OpenAI-base
  • Refactor

    • Providers delegated shared logic to new base/utils while preserving public APIs
  • Tests

    • Added comprehensive tests for utilities and OpenAI-compatible adapters

@AlemTuzlak AlemTuzlak requested a review from a team March 30, 2026 12:57
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 30, 2026

🚀 Changeset Version Preview

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

🟨 Minor bumps

Package Version Reason
@tanstack/ai-utils 0.1.0 → 0.2.0 Changeset
@tanstack/openai-base 0.1.0 → 0.2.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-anthropic 0.7.1 → 0.7.2 Changeset
@tanstack/ai-elevenlabs 0.1.2 → 0.1.3 Changeset
@tanstack/ai-fal 0.6.9 → 0.6.10 Changeset
@tanstack/ai-gemini 0.8.4 → 0.8.5 Changeset
@tanstack/ai-grok 0.6.3 → 0.6.4 Changeset
@tanstack/ai-groq 0.1.3 → 0.1.4 Changeset
@tanstack/ai-ollama 0.6.3 → 0.6.4 Changeset
@tanstack/ai-openai 0.7.2 → 0.7.3 Changeset
@tanstack/ai-openrouter 0.7.0 → 0.7.1 Changeset
@tanstack/smoke-tests-e2e 0.0.31 → 0.0.32 Dependent
@tanstack/tests-adapters 0.1.25 → 0.1.26 Dependent
ts-svelte-chat 0.1.25 → 0.1.26 Dependent
ts-vue-chat 0.1.25 → 0.1.26 Dependent

@nx-cloud
Copy link
Copy Markdown

nx-cloud bot commented Mar 30, 2026

View your CI Pipeline Execution ↗ for commit 19231f2

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 1m 40s View ↗
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 49s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-30 17:07:12 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 30, 2026

Open in StackBlitz

@tanstack/ai

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

@tanstack/ai-anthropic

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

@tanstack/ai-client

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

@tanstack/ai-devtools-core

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

@tanstack/ai-elevenlabs

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

@tanstack/ai-event-client

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

@tanstack/ai-fal

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

@tanstack/ai-gemini

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

@tanstack/ai-grok

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

@tanstack/ai-groq

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

@tanstack/ai-ollama

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

@tanstack/ai-openai

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

@tanstack/ai-openrouter

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

@tanstack/ai-preact

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

@tanstack/ai-react

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

@tanstack/ai-react-ui

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

@tanstack/ai-solid

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

@tanstack/ai-solid-ui

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

@tanstack/ai-svelte

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

@tanstack/ai-utils

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

@tanstack/ai-vue

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

@tanstack/ai-vue-ui

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

@tanstack/openai-base

npm i https://pkg.pr.new/TanStack/ai/@tanstack/openai-base@409

@tanstack/preact-ai-devtools

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

@tanstack/react-ai-devtools

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

@tanstack/solid-ai-devtools

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

commit: 19231f2

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

Added two new packages—@tanstack/ai-utils and @tanstack/openai-base—and refactored multiple provider packages to delegate ID generation, env-key resolution, schema transformation, and tool conversion to those new shared packages. Many adapters now extend OpenAI-compatible base adapters and numerous local tool/type implementations were replaced by re-exports.

Changes

Cohort / File(s) Summary
New ai-utils package
packages/typescript/ai-utils/...
New package exporting generateId, getApiKeyFromEnv, transformNullsToUndefined, ModelMeta/Modality types and defineModelMeta; includes tests, tsconfig, and vite config.
New openai-base package
packages/typescript/openai-base/...
New package adding OpenAI-compatible base adapters (Responses/ChatCompletions text, image, summarize, transcription, TTS, video), schema converter makeStructuredOutputCompatible, tool converters/types, client factory, tests, and build/config.
Provider dependency updates
packages/typescript/ai-*/package.json
packages/typescript/ai-anthropic/package.json, .../ai-elevenlabs/package.json, .../ai-fal/package.json, .../ai-gemini/package.json, .../ai-groq/package.json, .../ai-ollama/package.json, .../ai-openai/package.json, .../ai-openrouter/package.json
Added @tanstack/ai-utils (and where applicable @tanstack/openai-base) to dependencies and devDependencies.
Provider util refactors
packages/typescript/ai-*/src/utils/client.ts
ai-anthropic, ai-fal, ai-gemini, ai-groq, ai-ollama, ai-openrouter, ai-openai
Replaced local env-key resolution and ID generation with getApiKeyFromEnv and generateId from @tanstack/ai-utils; some providers add toCompatibleConfig helpers.
ai-openai: adapters & tools delegation
packages/typescript/ai-openai/src/adapters/..., packages/typescript/ai-openai/src/tools/*, packages/typescript/ai-openai/src/utils/*
Adapters now extend OpenAI-compatible base classes and remove local streaming/structured logic; many per-tool implementations removed and re-exported from @tanstack/openai-base; createOpenAIClient removed in favor of toCompatibleConfig.
ai-grok: adapter & tool refactor
packages/typescript/ai-grok/src/...
Text/image/summarize adapters switched to OpenAI-compatible base adapters; local streaming, structured-output, tool conversion, and schema logic removed or delegated; function-tool and tool-converter files deleted; added toCompatibleConfig and schema re-exports.
Schema & transforms delegation
packages/typescript/*/src/utils/schema-converter.ts, packages/typescript/ai-groq/src/utils/schema-converter.ts, packages/typescript/ai-openai/src/utils/schema-converter.ts
Local implementations of transformNullsToUndefined and structured-output converters replaced by re-exports/delegation to @tanstack/ai-utils and @tanstack/openai-base; some packages add normalization/pruning hooks.
Tools consolidation / re-exports
packages/typescript/ai-openai/src/tools/*packages/typescript/openai-base/src/tools/*
Many tool types and converters (function-tool, file-search, code-interpreter, image-generation, mcp, shell, web-search, apply-patch, etc.) moved to/openai-base and ai-openai now re-exports them.
Tests & changesets
packages/typescript/openai-base/tests/*, packages/typescript/ai-utils/tests/*, .changeset/*
Extensive new test suites for adapters, converters, and utils; added changeset files documenting new packages and provider refactor releases.
Misc provider test adjustments
packages/typescript/ai-grok/tests/*, packages/typescript/ai-groq/tests/*
Test refactors to use per-adapter mock injection and updated assertions for delegated error messages/keys.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Provider
    participant Utils
    Client->>Provider: request API key
    Provider->>Utils: getApiKeyFromEnv('ENV_VAR')
    Utils->>Utils: check globalThis.window.env / process.env
    alt key found in window.env
        Utils-->>Provider: return apiKey
    else key found in process.env
        Utils-->>Provider: return apiKey
    else
        Utils-->>Provider: throw Error("ENV_VAR not set")
    end
    Provider-->>Client: apiKey or throw
Loading
sequenceDiagram
    participant App as Client/App
    participant Adapter as ProviderAdapter
    participant Base as OpenAIBaseAdapter
    participant SDK as OpenAI SDK
    App->>Adapter: chatStream(options)
    Adapter->>Base: chatStream(options) (delegated)
    Base->>Base: mapOptionsToRequest()
    Base->>SDK: responses.create(request, stream=true)
    SDK-->>Base: AsyncIterable<chunks>
    Base->>Base: processStreamChunks() -> emit RUN_* / TEXT_* / TOOL_* events
    Base-->>Adapter: StreamChunk events
    Adapter-->>App: StreamChunk events forwarded
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related PRs

Suggested reviewers

  • AlemTuzlak

Poem

🐰
I hopped through files with nimble feet,
pulled shared bits into one tidy heap.
IDs and keys now hop in line,
base adapters bloom—reused, refined.
A carrot bouquet of cleaner code!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/extract-openai-base-and-ai-utils

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

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-openai/src/adapters/image.ts (1)

100-103: ⚠️ Potential issue | 🟡 Minor

Fix the env lookup order in the JSDoc.

getOpenAIApiKeyFromEnv() now prefers window.env before process.env, so this list documents the opposite behavior. The same copy appears in the sibling OpenAI factory helpers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-openai/src/adapters/image.ts` around lines 100 - 103,
JSDoc lists env lookup order incorrectly; update the comment for
getOpenAIApiKeyFromEnv to state it checks window.env (Browser injected env)
first, then process.env (Node.js), and apply the same change to the matching
OpenAI factory helper docs so both accurately reflect the current lookup order.
🟡 Minor comments (10)
packages/typescript/ai-utils/src/id.ts-1-5 (1)

1-5: ⚠️ Potential issue | 🟡 Minor

substring(7) may produce inconsistent or empty random segments.

Math.random().toString(36) generates strings of varying length (e.g., "0.5" for 0.5, or "0.abc123..." for typical values). Using substring(7) starts extraction at index 7, which can yield an empty string or very short segment for smaller random numbers.

Consider using substring(2, 10) to skip the "0." prefix and take a consistent 8-character segment:

🔧 Proposed fix
 export function generateId(prefix: string): string {
   const timestamp = Date.now()
-  const randomPart = Math.random().toString(36).substring(7)
+  const randomPart = Math.random().toString(36).substring(2, 10)
   return `${prefix}-${timestamp}-${randomPart}`
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-utils/src/id.ts` around lines 1 - 5, The random
segment in generateId can be empty because substring(7) may exceed the length of
Math.random().toString(36); update the randomPart generation in function
generateId to consistently extract characters after the "0." (for example use
Math.random().toString(36).substring(2, 10) to get an 8-character segment) so
the id always includes a predictable non-empty random portion; ensure the change
is applied to the randomPart variable in generateId and keep the rest of the
template literal intact.
packages/typescript/openai-base/src/tools/web-search-preview-tool.ts-12-17 (1)

12-17: ⚠️ Potential issue | 🟡 Minor

Inconsistent type field handling.

Other tool converters in this package hardcode the type value (e.g., type: 'apply_patch', type: 'image_generation'), but this converter reads metadata.type from the input. This could produce invalid output if metadata.type is undefined or incorrect.

🔧 Proposed fix for consistency
 export function convertWebSearchPreviewToolToAdapterFormat(
   tool: Tool,
 ): WebSearchPreviewTool {
   const metadata = tool.metadata as WebSearchPreviewTool
   return {
-    type: metadata.type,
+    type: 'web_search_preview',
     search_context_size: metadata.search_context_size,
     user_location: metadata.user_location,
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/tools/web-search-preview-tool.ts` around
lines 12 - 17, The returned object's type field should be hardcoded for
consistency rather than reading metadata.type; update the converter that builds
the returned object (the code using metadata: WebSearchPreviewTool and returning
{ type, search_context_size, user_location }) to set type to a fixed literal
(e.g., 'web_search_preview') and optionally fall back to that literal when
metadata.type is falsy, leaving the other fields (search_context_size and
user_location) as-is.
packages/typescript/ai-grok/package.json-42-52 (1)

42-52: ⚠️ Potential issue | 🟡 Minor

Remove redundant entries from devDependencies.

@tanstack/ai-utils and @tanstack/openai-base are runtime dependencies (imported in src files like adapters and utilities). They should only appear in dependencies, not duplicated in devDependencies. Other adapter packages in the monorepo (e.g., ai-groq) follow this pattern successfully.

Remove redundant devDependencies
   "devDependencies": {
-    "@tanstack/ai-utils": "workspace:*",
-    "@tanstack/openai-base": "workspace:*",
     "@vitest/coverage-v8": "4.0.14",
     "vite": "^7.2.7"
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-grok/package.json` around lines 42 - 52, Remove the
duplicate runtime packages from devDependencies in package.json so
`@tanstack/ai-utils` and `@tanstack/openai-base` only appear under "dependencies";
open the package.json in the ai-grok package, delete the two entries from the
"devDependencies" block, ensure the versions remain defined in the top-level
"dependencies" block, and then reinstall (npm/yarn/pnpm) to update the lockfile
consistent with other adapter packages like ai-groq.
packages/typescript/ai-groq/src/utils/client.ts-1-3 (1)

1-3: ⚠️ Potential issue | 🟡 Minor

Fix import order to satisfy ESLint (import/order).

Static analysis reports this ordering as invalid; moving the value import before the type import should resolve it.

🔧 Proposed fix
 import Groq_SDK from 'groq-sdk'
-import type { ClientOptions } from 'groq-sdk'
 import { generateId as _generateId, getApiKeyFromEnv } from '@tanstack/ai-utils'
+import type { ClientOptions } from 'groq-sdk'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-groq/src/utils/client.ts` around lines 1 - 3, ESLint
import/order flags the type import position; reorder the imports so value
imports come before the type-only import. Specifically, keep import Groq_SDK
from 'groq-sdk' and move the value import "import { generateId as _generateId,
getApiKeyFromEnv } from '@tanstack/ai-utils'" before the type import "import
type { ClientOptions } from 'groq-sdk'"; ensure Groq_SDK and the ai-utils value
import appear above the ClientOptions type-only import.
packages/typescript/openai-base/src/tools/code-interpreter-tool.ts-22-31 (1)

22-31: ⚠️ Potential issue | 🟡 Minor

Parameter naming is misleading and metadata structure may cause round-trip issues.

The parameter container: CodeInterpreterTool receives the entire CodeInterpreterTool object, not just the container portion. This creates a confusing API and potentially incorrect metadata structure.

When codeInterpreterTool(toolData) is called, it stores toolData (the full CodeInterpreterTool) under metadata.container. But convertCodeInterpreterToolToAdapterFormat expects metadata.container to be just the container value, not the full tool object.

Suggested fix
 /**
  * Creates a standard Tool from CodeInterpreterTool parameters
  */
-export function codeInterpreterTool(container: CodeInterpreterTool): Tool {
+export function codeInterpreterTool(toolData: CodeInterpreterTool): Tool {
   return {
     name: 'code_interpreter',
     description: 'Execute code in a sandboxed environment',
-    metadata: {
-      type: 'code_interpreter',
-      container,
-    },
+    metadata: toolData,
   }
 }

This aligns with the pattern used in webSearchTool where metadata stores the entire tool configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/tools/code-interpreter-tool.ts` around
lines 22 - 31, The codeInterpreterTool function currently takes a misleading
parameter named container (typed CodeInterpreterTool) and stores the whole tool
object under metadata.container, which breaks
convertCodeInterpreterToolToAdapterFormat expectations; rename the parameter to
toolData (or similar) and change the metadata assignment so metadata.container =
toolData.container (i.e., store only the container value), optionally keeping
the full tool under a separate key like metadata.tool if you need the full
config; update any references to the function signature accordingly.
packages/typescript/ai-utils/package.json-40-43 (1)

40-43: ⚠️ Potential issue | 🟡 Minor

Add vitest to devDependencies.

The test:lib script uses vitest run, but vitest is missing from devDependencies. While workspace hoisting (via pnpm-workspace.yaml) allows this to work in the monorepo, sibling packages like ai-vue and ai-solid explicitly declare vitest ^4.0.14. Add it to make the package consistent and more portable outside the workspace.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-utils/package.json` around lines 40 - 43, The
package.json devDependencies block is missing "vitest" even though the test:lib
script uses `vitest run`; update the package.json devDependencies to add
"vitest" with the same version used in siblings (e.g. "^4.0.14") so the package
is portable outside the workspace and consistent with other packages — modify
the "devDependencies" entry in packages/typescript/ai-utils/package.json
accordingly.
packages/typescript/openai-base/src/adapters/summarize.ts-51-53 (1)

51-53: ⚠️ Potential issue | 🟡 Minor

Don't return the same blank id from every summarize() call.

const id = '' makes every result indistinguishable for tracing, cache keys, and UI bookkeeping. Seed a synthetic id at minimum, then overwrite it if a provider id becomes available later.

💡 Proposed fix
+import { generateId } from '@tanstack/ai-utils'
 import { BaseSummarizeAdapter } from '@tanstack/ai/adapters'
-    const id = ''
+    let id = generateId()

Also applies to: 78-78

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/adapters/summarize.ts` around lines 51 -
53, The summarize() function currently sets const id = '' so every call returns
the same empty id; change it to generate a synthetic unique id (e.g., UUID or
timestamp+random) when initializing id and keep it mutable so that you can
overwrite it later if/when a provider returns a real id; update the declaration
for id in summarize() (and the similar spot around the later block that
currently uses '' at line ~78) to be initialized to a generated unique string
and ensure later assignment from provider response replaces that value.
packages/typescript/openai-base/src/adapters/image.ts-120-130 (1)

120-130: ⚠️ Potential issue | 🟡 Minor

Reject fractional and NaN image counts.

NaN and non-integers both bypass the current range check, so this can still forward n: NaN or n: 1.5 to the provider.

🛠️ Suggested fix
-    if (numberOfImages < 1 || numberOfImages > 10) {
+    if (
+      !Number.isInteger(numberOfImages) ||
+      numberOfImages < 1 ||
+      numberOfImages > 10
+    ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/adapters/image.ts` around lines 120 -
130, validateNumberOfImages currently only checks range and allows NaN or
fractional values through; update validateNumberOfImages to reject non-integer
and NaN inputs by verifying Number.isFinite(numberOfImages) and
Number.isInteger(numberOfImages) (or equivalent) before the range check, and
throw a clear Error if the value is NaN or not an integer (include the invalid
value in the error message) so calls with n: NaN or n: 1.5 are rejected.
packages/typescript/openai-base/src/types/provider-options.ts-7-7 (1)

7-7: ⚠️ Potential issue | 🟡 Minor

Use the repo's configured array style for stop.

This new shared type trips @typescript-eslint/array-type, so it will keep reporting a lint error until it's rewritten as Array<string>.

🛠️ Suggested fix
-  stop?: string | string[]
+  stop?: string | Array<string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/types/provider-options.ts` at line 7, The
'stop' property in the shared type currently uses the bracket array syntax
(stop?: string | string[]), which trips `@typescript-eslint/array-type`; change it
to use the repo's configured generic array style by updating the property to
stop?: string | Array<string> so the union remains but the array form satisfies
the lint rule and keeps the type meaning identical.
packages/typescript/openai-base/src/adapters/transcription.ts-78-85 (1)

78-85: ⚠️ Potential issue | 🟡 Minor

Don't drop avg_logprob = 0.

The truthy check turns a valid 0 into undefined, so perfect-confidence segments lose their confidence field.

🛠️ Suggested fix
-            confidence: seg.avg_logprob ? Math.exp(seg.avg_logprob) : undefined,
+            confidence:
+              seg.avg_logprob == null
+                ? undefined
+                : Math.exp(seg.avg_logprob),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/adapters/transcription.ts` around lines
78 - 85, The mapping in the segments conversion (response.segments?.map ->
TranscriptionSegment) currently uses a truthy check (avg_logprob ? Math.exp(...)
: undefined) which drops valid zero values; update the confidence assignment in
the segments mapping (inside the function that builds TranscriptionSegment
objects) to test for presence explicitly (e.g., seg.avg_logprob !==
undefined/null or typeof seg.avg_logprob === "number") and only then compute
Math.exp(seg.avg_logprob), so avg_logprob = 0 yields confidence = Math.exp(0)
instead of undefined.
🧹 Nitpick comments (14)
packages/typescript/openai-base/src/tools/computer-use-tool.ts (1)

23-31: Inconsistent function signature compared to other tool factories.

The computerUseTool function accepts the full ComputerUseTool type including type, while similar factories like imageGenerationTool use Omit<..., 'type'>. This requires callers to redundantly specify type: 'computer_use_preview'.

♻️ Proposed fix for consistency
-export function computerUseTool(toolData: ComputerUseTool): Tool {
+export function computerUseTool(
+  toolData: Omit<ComputerUseTool, 'type'>,
+): Tool {
   return {
     name: 'computer_use_preview',
     description: 'Control a virtual computer',
     metadata: {
       ...toolData,
     },
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/tools/computer-use-tool.ts` around lines
23 - 31, Function signature inconsistent: change the parameter of
computerUseTool from ComputerUseTool to Omit<ComputerUseTool, 'type'> so callers
don't have to pass type. Update the factory function computerUseTool to accept
toolData: Omit<ComputerUseTool, 'type'>, keep returning the same object with
name: 'computer_use_preview' and metadata: { ...toolData }, and ensure any call
sites that previously passed type no longer do so (or remove the redundant type
property).
packages/typescript/openai-base/src/tools/file-search-tool.ts (1)

15-26: Consider adding validation or type guard in convertFileSearchToolToAdapterFormat.

The function casts tool.metadata to OpenAI.Responses.FileSearchTool without validation, while fileSearchTool validates max_num_results. If a Tool with invalid/mismatched metadata is passed, this could produce malformed API requests silently.

Consider adding a runtime check or documenting that callers must ensure the Tool was created via fileSearchTool:

♻️ Optional: Add validation for consistency
 export function convertFileSearchToolToAdapterFormat(
   tool: Tool,
 ): OpenAI.Responses.FileSearchTool {
   const metadata = tool.metadata as OpenAI.Responses.FileSearchTool
+  validateMaxNumResults(metadata.max_num_results)
   return {
     type: 'file_search',
     vector_store_ids: metadata.vector_store_ids,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/tools/file-search-tool.ts` around lines
15 - 26, convertFileSearchToolToAdapterFormat currently casts tool.metadata to
OpenAI.Responses.FileSearchTool without validation; add a runtime type guard or
validation at the start of convertFileSearchToolToAdapterFormat to verify
required fields (e.g., vector_store_ids, max_num_results, ranking_options,
filters) and that max_num_results meets the same constraints enforced by
fileSearchTool, and throw or return a clear error if validation fails so
malformed metadata cannot produce invalid adapter requests.
packages/typescript/ai-gemini/package.json (1)

42-54: Same redundancy concern: @tanstack/ai-utils in both dependency sections.

As noted for ai-grok, the package appears in both dependencies and devDependencies. Consider removing from devDependencies unless there's a specific monorepo tooling requirement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-gemini/package.json` around lines 42 - 54, The
package.json for ai-gemini lists "@tanstack/ai-utils" in both "dependencies" and
"devDependencies", creating redundancy; remove the duplicate entry from
"devDependencies" so "@tanstack/ai-utils" only appears once (leave it in
"dependencies"), then run the package manager install to refresh the lockfile
and ensure the workspace resolution is correct; locate the entries by the
symbols "dependencies", "devDependencies", and the package name
"@tanstack/ai-utils" in the package.json to make the change.
packages/typescript/ai-utils/tests/env.test.ts (1)

23-27: Add one regression test for window.env precedence.

Given the shared helper is isomorphic, adding an explicit browser-env precedence case would prevent future regressions.

Proposed test addition
 describe('getApiKeyFromEnv', () => {
   afterEach(() => {
     vi.unstubAllEnvs()
+    delete (globalThis as { window?: { env?: Record<string, string> } }).window
   })
@@
   it('should include the env var name in the error message', () => {
     expect(() => getApiKeyFromEnv('MY_PROVIDER_API_KEY')).toThrow(
       'MY_PROVIDER_API_KEY',
     )
   })
+
+  it('should prefer globalThis.window.env over process.env when both exist', () => {
+    vi.stubEnv('TEST_API_KEY', 'from-process')
+    ;(globalThis as { window?: { env?: Record<string, string> } }).window = {
+      env: { TEST_API_KEY: 'from-window' },
+    }
+    expect(getApiKeyFromEnv('TEST_API_KEY')).toBe('from-window')
+  })
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-utils/tests/env.test.ts` around lines 23 - 27, Add a
regression test in the existing env.test.ts that verifies browser (window.env)
precedence for the isomorphic helper: set process.env.MY_PROVIDER_API_KEY to one
value and set global.window = { env: { MY_PROVIDER_API_KEY: 'browser_value' } }
then assert getApiKeyFromEnv('MY_PROVIDER_API_KEY') returns 'browser_value';
finally clean up/reset global.window and process.env to avoid test pollution.
Use the getApiKeyFromEnv symbol and window.env/process.env references so the
test prevents regressions where process.env accidentally overrides browser env.
packages/typescript/ai-utils/tests/model-meta.test.ts (1)

5-106: Add tests for the remaining validation branches.

The suite doesn’t currently assert the max_output_tokens positivity check or negative pricing.input.cached path from defineModelMeta.

✅ Suggested test additions
 describe('defineModelMeta', () => {
+  it('should throw for zero max output tokens', () => {
+    expect(() =>
+      defineModelMeta({
+        name: 'test',
+        supports: {
+          input: ['text'] as Array<Modality>,
+          output: ['text'] as Array<Modality>,
+        },
+        max_output_tokens: 0,
+      }),
+    ).toThrow('max_output_tokens')
+  })
+
+  it('should throw for negative cached input pricing', () => {
+    expect(() =>
+      defineModelMeta({
+        name: 'test',
+        supports: {
+          input: ['text'] as Array<Modality>,
+          output: ['text'] as Array<Modality>,
+        },
+        pricing: {
+          input: { normal: 1, cached: -0.1 },
+          output: { normal: 1 },
+        },
+      }),
+    ).toThrow('pricing.input.cached')
+  })
[review_comment_end]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-utils/tests/model-meta.test.ts` around lines 5 - 106,
Tests are missing coverage for the positivity check on max_output_tokens and the
validation that rejects negative pricing.input.cached in defineModelMeta; add
two new test cases in model-meta.test.ts that call defineModelMeta with (1)
max_output_tokens set to 0 (or negative) and assert it throws
'max_output_tokens', and (2) pricing.input.cached set to a negative value and
assert it throws 'pricing' (referencing the defineModelMeta function and the
max_output_tokens and pricing.input.cached fields to locate the validation
branches).
packages/typescript/ai-utils/tests/transforms.test.ts (1)

1-51: Good test coverage for transformNullsToUndefined.

The tests comprehensively cover the main scenarios. One edge case that might be worth adding: arrays containing null values directly (e.g., [1, null, 3]) to verify whether nulls in arrays are filtered, converted, or preserved.

💡 Optional: Add test for direct null values in arrays
+  it('should handle arrays with direct null values', () => {
+    const result = transformNullsToUndefined({
+      items: [1, null, 'hello', null],
+    })
+    // Verify expected behavior - adjust assertion based on intended behavior
+    expect(result.items).toEqual([1, null, 'hello', null]) // or filtered?
+  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-utils/tests/transforms.test.ts` around lines 1 - 51,
Add a test case to verify how transformNullsToUndefined treats direct null
entries inside arrays (e.g., items: [1, null, 3]) by calling
transformNullsToUndefined with an object or array containing such an array and
asserting the expected behavior (whether nulls are removed, converted to
undefined, or preserved). Place the new spec alongside existing cases in
transforms.test.ts and reference transformNullsToUndefined in the test name and
assertions so reviewers can see the intended outcome clearly.
packages/typescript/ai-fal/src/utils/client.ts (1)

19-22: Dead code after migration to shared utility.

The if (!apiKey) check is now unreachable. According to the AI summary, getApiKeyFromEnv throws an error when the key is not found, so getFalApiKeyFromEnv() will throw before returning a falsy value. This error handling block is dead code.

Consider removing the redundant check or catching the exception from getFalApiKeyFromEnv() if you want to customize the error message:

Option 1: Remove dead code
 export function configureFalClient(config?: FalClientConfig): void {
   if (config?.proxyUrl) {
     fal.config({
       proxyUrl: config.proxyUrl,
     })
   } else {
     const apiKey = config?.apiKey ?? getFalApiKeyFromEnv()
-    if (!apiKey) {
-      throw new Error('API key is required')
-    }
     fal.config({
       credentials: apiKey,
     })
   }
 }
Option 2: Wrap for custom error message
   } else {
-    const apiKey = config?.apiKey ?? getFalApiKeyFromEnv()
-    if (!apiKey) {
-      throw new Error('API key is required')
-    }
+    let apiKey = config?.apiKey
+    if (!apiKey) {
+      try {
+        apiKey = getFalApiKeyFromEnv()
+      } catch {
+        throw new Error('API key is required')
+      }
+    }
     fal.config({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-fal/src/utils/client.ts` around lines 19 - 22, The if
(!apiKey) branch is dead because getFalApiKeyFromEnv() throws when the key is
missing; remove the redundant check or wrap the call in a try/catch to provide a
custom error message. Specifically, update the apiKey assignment that uses
config?.apiKey ?? getFalApiKeyFromEnv(): either delete the subsequent if
(!apiKey) throw new Error('API key is required') block, or change the fallback
call to be try { getFalApiKeyFromEnv() } catch (err) { throw new Error(`Fal API
key missing: ${err.message}`) } (adjusting throw text as desired) so error
handling around getFalApiKeyFromEnv and the apiKey variable is correct.
packages/typescript/ai-gemini/src/utils/client.ts (1)

24-28: Consider improving the fallback error message.

When both GOOGLE_API_KEY and GEMINI_API_KEY are missing, the error will only mention GEMINI_API_KEY. Users expecting GOOGLE_API_KEY might be confused.

Suggested improvement
 export function getGeminiApiKeyFromEnv(): string {
   try {
     return getApiKeyFromEnv('GOOGLE_API_KEY')
   } catch {
-    return getApiKeyFromEnv('GEMINI_API_KEY')
+    try {
+      return getApiKeyFromEnv('GEMINI_API_KEY')
+    } catch {
+      throw new Error('GOOGLE_API_KEY or GEMINI_API_KEY not found in environment')
+    }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-gemini/src/utils/client.ts` around lines 24 - 28, The
fallback currently calls getApiKeyFromEnv('GOOGLE_API_KEY') and on any error
returns getApiKeyFromEnv('GEMINI_API_KEY'), which leads to an error that only
mentions GEMINI_API_KEY when both are missing; update the logic to attempt the
second call inside its own try/catch (or capture both errors) and when both
attempts fail throw a single error that clearly lists both environment variables
(GOOGLE_API_KEY and GEMINI_API_KEY) and their failure reasons so callers see
that both keys are missing/invalid; reference getApiKeyFromEnv('GOOGLE_API_KEY')
and getApiKeyFromEnv('GEMINI_API_KEY') when implementing the combined error.
packages/typescript/openai-base/src/tools/web-search-tool.ts (1)

9-12: Consider adding defensive checks for metadata.

The function directly casts and returns tool.metadata without validation. If metadata is undefined or malformed, this will silently pass invalid data to OpenAI.

Suggested defensive check
 export function convertWebSearchToolToAdapterFormat(tool: Tool): WebSearchTool {
   const metadata = tool.metadata as WebSearchTool
+  if (!metadata || metadata.type !== 'web_search') {
+    throw new Error('Invalid web_search tool: missing or invalid metadata')
+  }
   return metadata
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/tools/web-search-tool.ts` around lines 9
- 12, convertWebSearchToolToAdapterFormat currently casts tool.metadata to
WebSearchTool without validation; add defensive checks to ensure tool.metadata
exists and has the expected fields before returning it. In
convertWebSearchToolToAdapterFormat, validate that tool.metadata is not
undefined/null and that required WebSearchTool properties (e.g., any unique keys
your type requires) are present and correctly typed, and either throw a clear
error or return a safe default/converted object when validation fails; reference
the Tool type, WebSearchTool type, and the metadata variable to locate where to
add these checks.
packages/typescript/openai-base/src/tools/tool-converter.ts (1)

16-28: Consider adding runtime exhaustiveness check to guard against drift.

The SPECIAL_TOOL_NAMES set and the switch statement must stay synchronized. If a name is added to the set but the corresponding case is missing, the tool silently falls through to the function tool converter at line 66—potentially incorrect behavior.

A defensive default with an error would catch this during development:

♻️ Proposed exhaustiveness guard
         case 'custom':
           return convertCustomToolToAdapterFormat(tool)
+        default:
+          // This should never be reached if SPECIAL_TOOL_NAMES and switch are in sync
+          throw new Error(`Unhandled special tool: ${toolName}`)
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/tools/tool-converter.ts` around lines 16
- 28, The SPECIAL_TOOL_NAMES set and the switch in the tool conversion logic
must be kept in sync to avoid falling back to functionToolConverter incorrectly;
add a defensive exhaustiveness guard by updating the switch that branches on
tool names (the same switch that currently falls through to
functionToolConverter) to include a default branch that throws or logs an error
referencing the unexpected tool name, and also add a runtime check that every
entry in SPECIAL_TOOL_NAMES is covered by a corresponding switch case (or vice
versa) so any drift fails fast during development.
packages/typescript/openai-base/tests/schema-converter.test.ts (1)

32-45: Add test case for already-nullable fields to improve coverage.

The implementation correctly prevents adding duplicate 'null' via the !prop.type.includes('null') check at line 55 of the converter, but the test suite doesn't verify this edge case. Add a test where a field is already nullable (type: ['string', 'null']) to confirm it remains unchanged.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/tests/schema-converter.test.ts` around lines
32 - 45, Add a test to verify makeStructuredOutputCompatible preserves existing
nullable types: create a schema where one property already has type
['string','null'] (e.g., nickname) and required includes only name, call
makeStructuredOutputCompatible(schema, ['name']) and assert that nickname.type
remains ['string','null'] (and name stays 'string'); reference the existing test
in schema-converter.test.ts and the makeStructuredOutputCompatible function to
place the new case next to the current optional-fields test.
packages/typescript/openai-base/src/tools/custom-tool.ts (1)

9-17: Tool converters should validate metadata schema at runtime using Zod, consistent with input/output validation guidelines.

All tool converters in openai-base/src/tools/ (custom-tool, web-search-tool, mcp-tool, file-search-tool, code-interpreter-tool, computer-use-tool, image-generation-tool, web-search-preview-tool) cast tool.metadata without runtime validation. This pattern risks silently propagating undefined values if metadata structure is malformed. Consider adding Zod schema validation for each tool type to ensure compile-time type safety and runtime correctness, aligning with the schema validation guideline for tool definitions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/tools/custom-tool.ts` around lines 9 -
17, The convertCustomToolToAdapterFormat function currently casts tool.metadata
to CustomTool without runtime checks; add a Zod schema for the CustomTool
metadata, validate tool.metadata at the start of
convertCustomToolToAdapterFormat (using schema.parse or safeParse), and handle
validation failures by throwing or logging a descriptive error so malformed
metadata doesn't propagate; reference the convertCustomToolToAdapterFormat
function, the metadata variable and the CustomTool shape when adding the Zod
schema and validation call.
packages/typescript/ai-openai/src/utils/client.ts (1)

38-41: Consolidate the config type to prevent silent interface drift.

Both OpenAIClientConfig and OpenAICompatibleClientConfig are structurally identical. The double-cast pattern at line 41 (and its duplicate in ai-grok) bypasses TypeScript's structural checking. If either interface diverges, the mismatch only surfaces at runtime or in consuming code downstream.

Options:

  • Export OpenAICompatibleClientConfig from openai-base and reuse it directly (eliminate the local OpenAIClientConfig wrapper).
  • Create an explicit field-by-field mapper function that documents the boundary contract.
  • Or, define a shared internal config type that both packages import.

The current approach leaves the package boundary unchecked.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-openai/src/utils/client.ts` around lines 38 - 41, The
double-cast in toCompatibleConfig permits silent interface drift between
OpenAIClientConfig and OpenAICompatibleClientConfig; fix by consolidating types
or mapping explicitly: either import and use a single shared config type
exported from openai-base (replace
OpenAIClientConfig/OpenAICompatibleClientConfig uses and remove the cast in
toCompatibleConfig), or implement a field-by-field mapper inside
toCompatibleConfig that constructs an OpenAICompatibleClientConfig from the
OpenAIClientConfig properties (no unknown/as casts) so the boundary is explicit;
update all callers of toCompatibleConfig accordingly (function name
toCompatibleConfig is the change point).
packages/typescript/openai-base/src/adapters/responses-tool-converter.ts (1)

46-49: Avoid mutating the schema object returned by schemaConverter().

If a custom converter returns a shared object, forcing additionalProperties in place leaks state across later conversions. Build a new object before tightening strict mode.

♻️ Proposed fix
-  const jsonSchema = schemaConverter(inputSchema, inputSchema.required || [])
-
-  // Ensure additionalProperties is false for strict mode
-  jsonSchema.additionalProperties = false
+  const jsonSchema = {
+    ...schemaConverter(inputSchema, inputSchema.required || []),
+    additionalProperties: false,
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/adapters/responses-tool-converter.ts`
around lines 46 - 49, The code mutates the object returned by
schemaConverter(inputSchema, ...) by setting jsonSchema.additionalProperties =
false which can leak state; instead create a new object (shallow or deep clone
as appropriate) from the schemaConverter result and set
additionalProperties=false on that new object before returning/using it. Locate
the usage of schemaConverter and the jsonSchema variable in
responses-tool-converter.ts and replace the in-place mutation with constructing
a new schema object (e.g., via object spread or a deep clone helper) then set
additionalProperties on that new object so the original converter output is not
modified.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/typescript/ai-groq/src/utils/schema-converter.ts`:
- Around line 18-37: The post-pass in removeEmptyRequired only recurses into
result.properties and result.items, so empty required arrays inside combinators
and other schema-valued branches remain; update removeEmptyRequired to also
detect and recurse into combinator arrays (result.anyOf, result.oneOf,
result.allOf) by mapping each entry through removeEmptyRequired, and recurse
into object-valued branches like result.additionalProperties (and any other
schema-valued fields you spot) by calling removeEmptyRequired on them so empty
required arrays are pruned throughout the schema tree.

In `@packages/typescript/openai-base/src/adapters/chat-completions-text.ts`:
- Around line 412-448: The adapter drops provider-specific settings by not
forwarding options.modelOptions in mapOptionsToRequest; update
mapOptionsToRequest (in chat-completions-text.ts) to merge options.modelOptions
into the returned request object (e.g., spread options.modelOptions after the
known top-level fields or merge with tools/stream fields) so provider-specific
chat-completions settings reach the request; ensure the merged keys are
typed/compatible with
OpenAI_SDK.Chat.Completions.ChatCompletionCreateParamsStreaming and cast where
necessary (e.g., when adding tools or stream_options) to avoid type errors.
- Around line 141-152: structuredOutput is passing requestParams (from
mapOptionsToRequest) which may contain stream_options (set by
mapOptionsToRequest) into a non-stream request (stream: false); remove or omit
stream_options when calling structuredOutput so stream_options is only sent when
stream: true. Update structuredOutput (the call that creates
chat.completions.create) to shallow-copy requestParams and delete or
conditionally exclude requestParams.stream_options when stream is false, or
modify mapOptionsToRequest to attach stream_options only when options specify
streaming; reference the functions mapOptionsToRequest and
structuredOutput/requestParams to locate the change.
- Around line 75-81: mapOptionsToRequest currently only returns request
parameters and ignores the optional request/requestInit in TextOptions; update
mapOptionsToRequest to return a tuple or object containing both the
requestParams and requestOptions (derived from TextOptions.request), then change
both calls to this.client.chat.completions.create(...) (the streaming call that
feeds processStreamChunks and the structured-output create call) to pass the
requestOptions as the second argument to create(). Ensure requestOptions is the
same forwarded Request | RequestInit so per-request headers and other
RequestOptions are preserved.

In `@packages/typescript/openai-base/src/adapters/responses-text.ts`:
- Around line 79-82: The tool-call metadata map (toolCallMetadata) currently
only stores index/name/started so emitted tool-call events use the internal id;
update the map value type (used in responses-text adapter) to also store the
Responses API function_call.call_id when you parse function_call items, then
change all emissions of TOOL_CALL_START, TOOL_CALL_ARGS, and TOOL_CALL_END to
include toolCallId: call_id (not the internal id) so that callers using
convertMessagesToInput() can correlate results via function_call_output.call_id;
reference the toolCallMetadata map and the event emission sites in
responses-text.ts and ensure the stored call_id is propagated into those events.

In `@packages/typescript/openai-base/src/adapters/transcription.ts`:
- Around line 61-65: The transcription base currently sets useVerbose true
whenever responseFormat is falsy and model !== 'whisper-1', which hardcodes an
OpenAI model name into the shared adapter; remove that heuristic from the base
by making useVerbose only true when responseFormat === 'verbose_json', and move
any model-specific defaults into the OpenAI-specific adapter via a protected
override (e.g., add a protected method like shouldDefaultToVerbose(model) that
the OpenAI adapter overrides to enable the legacy behavior). Update uses of
responseFormat, model, and useVerbose in
packages/typescript/openai-base/src/adapters/transcription.ts to rely on the
base opt-in rule and implement the override in the OpenAI adapter so non-OpenAI
providers remain provider-agnostic.
- Around line 105-147: The prepareAudioFile method uses ArrayBuffer and atob
without runtime guards; update prepareAudioFile to check for typeof ArrayBuffer
!== 'undefined' before using ArrayBuffer (in the ArrayBuffer branch) and check
typeof atob === 'function' before calling atob (in both data URL and raw base64
branches); if those globals are unavailable, either delegate to a runtime-safe
helper or throw a clear error indicating the missing runtime capability (mention
prepareAudioFile to locate the change) so the method behaves consistently with
the existing File/Blob guards.

In `@packages/typescript/openai-base/src/adapters/tts.ts`:
- Around line 49-56: The returned format/contentType is derived from the
original `format` variable even though `modelOptions` (spread into `request`)
can override `response_format`; update the code around the `request:
OpenAI_SDK.Audio.SpeechCreateParams` construction so the final returned `format`
and `contentType` are computed from the merged `request` (e.g., read
`request.response_format` after spreading `modelOptions`, falling back to a
default like 'mp3'), and use that derived value for the response payload
metadata (affects the same logic referenced on lines ~65-66).

In `@packages/typescript/openai-base/src/adapters/video.ts`:
- Around line 131-139: The method probes should check for the OpenAI SDK's
videos.downloadContent(jobId) entrypoint instead of content/getContent/download
so update the detection to call client.videos.downloadContent(jobId) when
available (fall back to client.videos.retrieve(jobId) only otherwise) and avoid
always falling through to the manual fetch; also fix the expires_at conversion
where new Date(expires_at) is used (in the branches handling
videoInfo.expires_at and the manual download path) by multiplying the seconds
timestamp by 1000 (new Date(expires_at * 1000)) so expiration dates are correct.
- Around line 140-146: The code returns expiresAt using new
Date(videoInfo.expires_at) but OpenAI's expires_at is in seconds, while Date
expects milliseconds; update the construction in the adapter (the block using
videoInfo and jobId) to convert seconds to milliseconds (e.g., multiply the
numeric expires_at by 1000 or use Number(...) * 1000) before passing to new
Date, and preserve the existing undefined handling when expires_at is absent.

In `@packages/typescript/openai-base/src/tools/mcp-tool.ts`:
- Around line 18-24: In convertMCPToolToAdapterFormat the runtime metadata may
include a type field that currently gets overridden by spreading metadata before
setting type, allowing non-'mcp' types to leak through; fix by ensuring the
'type' property is applied after spreading metadata (or by creating a shallow
copy of metadata with any existing type removed) so the resulting MCPTool always
has type: 'mcp' regardless of tool.metadata contents; reference
convertMCPToolToAdapterFormat, Tool, MCPTool and the local metadata variable
when making this change.

In `@packages/typescript/openai-base/src/utils/schema-converter.ts`:
- Around line 26-66: The branch logic in makeStructuredOutputCompatible
currently recurses and assigns converted children directly, so optional fields
(wasOptional) inside object/array/anyOf branches lose nullability when
result.required is set; change the implementation to always produce a single
convertedProp for each propName (for object: call
makeStructuredOutputCompatible(prop, prop.required||[] ) and assign to
convertedProp; for array: set convertedProp = { ...prop, items:
makeStructuredOutputCompatible(prop.items, prop.items.required||[]) }; for
anyOf: convertedProp = makeStructuredOutputCompatible(prop, prop.required||[]) ;
for oneOf keep throwing) and only after that apply the wasOptional null-widening
transformation to convertedProp (if prop.type exists and not array -> type =
[type,'null']; if prop.type is array and missing 'null' -> append 'null');
finally set properties[propName] = convertedProp and then set
result.properties/result.required/additionalProperties as before.
- Around line 12-24: The function makeStructuredOutputCompatible currently
treats all fields as optional when originalRequired defaults to [], which can
incorrectly widen originally required properties; change the optionality source
to use schema.required when originalRequired is not supplied—inside
makeStructuredOutputCompatible, derive a working required list (e.g., const
requiredSource = originalRequired.length ? originalRequired : (schema.required
?? [])) and use requiredSource instead of originalRequired when computing
wasOptional for each propName, ensuring originally required fields are not
widened to include null before result.required is rewritten.

---

Outside diff comments:
In `@packages/typescript/ai-openai/src/adapters/image.ts`:
- Around line 100-103: JSDoc lists env lookup order incorrectly; update the
comment for getOpenAIApiKeyFromEnv to state it checks window.env (Browser
injected env) first, then process.env (Node.js), and apply the same change to
the matching OpenAI factory helper docs so both accurately reflect the current
lookup order.

---

Minor comments:
In `@packages/typescript/ai-grok/package.json`:
- Around line 42-52: Remove the duplicate runtime packages from devDependencies
in package.json so `@tanstack/ai-utils` and `@tanstack/openai-base` only appear
under "dependencies"; open the package.json in the ai-grok package, delete the
two entries from the "devDependencies" block, ensure the versions remain defined
in the top-level "dependencies" block, and then reinstall (npm/yarn/pnpm) to
update the lockfile consistent with other adapter packages like ai-groq.

In `@packages/typescript/ai-groq/src/utils/client.ts`:
- Around line 1-3: ESLint import/order flags the type import position; reorder
the imports so value imports come before the type-only import. Specifically,
keep import Groq_SDK from 'groq-sdk' and move the value import "import {
generateId as _generateId, getApiKeyFromEnv } from '@tanstack/ai-utils'" before
the type import "import type { ClientOptions } from 'groq-sdk'"; ensure Groq_SDK
and the ai-utils value import appear above the ClientOptions type-only import.

In `@packages/typescript/ai-utils/package.json`:
- Around line 40-43: The package.json devDependencies block is missing "vitest"
even though the test:lib script uses `vitest run`; update the package.json
devDependencies to add "vitest" with the same version used in siblings (e.g.
"^4.0.14") so the package is portable outside the workspace and consistent with
other packages — modify the "devDependencies" entry in
packages/typescript/ai-utils/package.json accordingly.

In `@packages/typescript/ai-utils/src/id.ts`:
- Around line 1-5: The random segment in generateId can be empty because
substring(7) may exceed the length of Math.random().toString(36); update the
randomPart generation in function generateId to consistently extract characters
after the "0." (for example use Math.random().toString(36).substring(2, 10) to
get an 8-character segment) so the id always includes a predictable non-empty
random portion; ensure the change is applied to the randomPart variable in
generateId and keep the rest of the template literal intact.

In `@packages/typescript/openai-base/src/adapters/image.ts`:
- Around line 120-130: validateNumberOfImages currently only checks range and
allows NaN or fractional values through; update validateNumberOfImages to reject
non-integer and NaN inputs by verifying Number.isFinite(numberOfImages) and
Number.isInteger(numberOfImages) (or equivalent) before the range check, and
throw a clear Error if the value is NaN or not an integer (include the invalid
value in the error message) so calls with n: NaN or n: 1.5 are rejected.

In `@packages/typescript/openai-base/src/adapters/summarize.ts`:
- Around line 51-53: The summarize() function currently sets const id = '' so
every call returns the same empty id; change it to generate a synthetic unique
id (e.g., UUID or timestamp+random) when initializing id and keep it mutable so
that you can overwrite it later if/when a provider returns a real id; update the
declaration for id in summarize() (and the similar spot around the later block
that currently uses '' at line ~78) to be initialized to a generated unique
string and ensure later assignment from provider response replaces that value.

In `@packages/typescript/openai-base/src/adapters/transcription.ts`:
- Around line 78-85: The mapping in the segments conversion
(response.segments?.map -> TranscriptionSegment) currently uses a truthy check
(avg_logprob ? Math.exp(...) : undefined) which drops valid zero values; update
the confidence assignment in the segments mapping (inside the function that
builds TranscriptionSegment objects) to test for presence explicitly (e.g.,
seg.avg_logprob !== undefined/null or typeof seg.avg_logprob === "number") and
only then compute Math.exp(seg.avg_logprob), so avg_logprob = 0 yields
confidence = Math.exp(0) instead of undefined.

In `@packages/typescript/openai-base/src/tools/code-interpreter-tool.ts`:
- Around line 22-31: The codeInterpreterTool function currently takes a
misleading parameter named container (typed CodeInterpreterTool) and stores the
whole tool object under metadata.container, which breaks
convertCodeInterpreterToolToAdapterFormat expectations; rename the parameter to
toolData (or similar) and change the metadata assignment so metadata.container =
toolData.container (i.e., store only the container value), optionally keeping
the full tool under a separate key like metadata.tool if you need the full
config; update any references to the function signature accordingly.

In `@packages/typescript/openai-base/src/tools/web-search-preview-tool.ts`:
- Around line 12-17: The returned object's type field should be hardcoded for
consistency rather than reading metadata.type; update the converter that builds
the returned object (the code using metadata: WebSearchPreviewTool and returning
{ type, search_context_size, user_location }) to set type to a fixed literal
(e.g., 'web_search_preview') and optionally fall back to that literal when
metadata.type is falsy, leaving the other fields (search_context_size and
user_location) as-is.

In `@packages/typescript/openai-base/src/types/provider-options.ts`:
- Line 7: The 'stop' property in the shared type currently uses the bracket
array syntax (stop?: string | string[]), which trips
`@typescript-eslint/array-type`; change it to use the repo's configured generic
array style by updating the property to stop?: string | Array<string> so the
union remains but the array form satisfies the lint rule and keeps the type
meaning identical.

---

Nitpick comments:
In `@packages/typescript/ai-fal/src/utils/client.ts`:
- Around line 19-22: The if (!apiKey) branch is dead because
getFalApiKeyFromEnv() throws when the key is missing; remove the redundant check
or wrap the call in a try/catch to provide a custom error message. Specifically,
update the apiKey assignment that uses config?.apiKey ?? getFalApiKeyFromEnv():
either delete the subsequent if (!apiKey) throw new Error('API key is required')
block, or change the fallback call to be try { getFalApiKeyFromEnv() } catch
(err) { throw new Error(`Fal API key missing: ${err.message}`) } (adjusting
throw text as desired) so error handling around getFalApiKeyFromEnv and the
apiKey variable is correct.

In `@packages/typescript/ai-gemini/package.json`:
- Around line 42-54: The package.json for ai-gemini lists "@tanstack/ai-utils"
in both "dependencies" and "devDependencies", creating redundancy; remove the
duplicate entry from "devDependencies" so "@tanstack/ai-utils" only appears once
(leave it in "dependencies"), then run the package manager install to refresh
the lockfile and ensure the workspace resolution is correct; locate the entries
by the symbols "dependencies", "devDependencies", and the package name
"@tanstack/ai-utils" in the package.json to make the change.

In `@packages/typescript/ai-gemini/src/utils/client.ts`:
- Around line 24-28: The fallback currently calls
getApiKeyFromEnv('GOOGLE_API_KEY') and on any error returns
getApiKeyFromEnv('GEMINI_API_KEY'), which leads to an error that only mentions
GEMINI_API_KEY when both are missing; update the logic to attempt the second
call inside its own try/catch (or capture both errors) and when both attempts
fail throw a single error that clearly lists both environment variables
(GOOGLE_API_KEY and GEMINI_API_KEY) and their failure reasons so callers see
that both keys are missing/invalid; reference getApiKeyFromEnv('GOOGLE_API_KEY')
and getApiKeyFromEnv('GEMINI_API_KEY') when implementing the combined error.

In `@packages/typescript/ai-openai/src/utils/client.ts`:
- Around line 38-41: The double-cast in toCompatibleConfig permits silent
interface drift between OpenAIClientConfig and OpenAICompatibleClientConfig; fix
by consolidating types or mapping explicitly: either import and use a single
shared config type exported from openai-base (replace
OpenAIClientConfig/OpenAICompatibleClientConfig uses and remove the cast in
toCompatibleConfig), or implement a field-by-field mapper inside
toCompatibleConfig that constructs an OpenAICompatibleClientConfig from the
OpenAIClientConfig properties (no unknown/as casts) so the boundary is explicit;
update all callers of toCompatibleConfig accordingly (function name
toCompatibleConfig is the change point).

In `@packages/typescript/ai-utils/tests/env.test.ts`:
- Around line 23-27: Add a regression test in the existing env.test.ts that
verifies browser (window.env) precedence for the isomorphic helper: set
process.env.MY_PROVIDER_API_KEY to one value and set global.window = { env: {
MY_PROVIDER_API_KEY: 'browser_value' } } then assert
getApiKeyFromEnv('MY_PROVIDER_API_KEY') returns 'browser_value'; finally clean
up/reset global.window and process.env to avoid test pollution. Use the
getApiKeyFromEnv symbol and window.env/process.env references so the test
prevents regressions where process.env accidentally overrides browser env.

In `@packages/typescript/ai-utils/tests/model-meta.test.ts`:
- Around line 5-106: Tests are missing coverage for the positivity check on
max_output_tokens and the validation that rejects negative pricing.input.cached
in defineModelMeta; add two new test cases in model-meta.test.ts that call
defineModelMeta with (1) max_output_tokens set to 0 (or negative) and assert it
throws 'max_output_tokens', and (2) pricing.input.cached set to a negative value
and assert it throws 'pricing' (referencing the defineModelMeta function and the
max_output_tokens and pricing.input.cached fields to locate the validation
branches).

In `@packages/typescript/ai-utils/tests/transforms.test.ts`:
- Around line 1-51: Add a test case to verify how transformNullsToUndefined
treats direct null entries inside arrays (e.g., items: [1, null, 3]) by calling
transformNullsToUndefined with an object or array containing such an array and
asserting the expected behavior (whether nulls are removed, converted to
undefined, or preserved). Place the new spec alongside existing cases in
transforms.test.ts and reference transformNullsToUndefined in the test name and
assertions so reviewers can see the intended outcome clearly.

In `@packages/typescript/openai-base/src/adapters/responses-tool-converter.ts`:
- Around line 46-49: The code mutates the object returned by
schemaConverter(inputSchema, ...) by setting jsonSchema.additionalProperties =
false which can leak state; instead create a new object (shallow or deep clone
as appropriate) from the schemaConverter result and set
additionalProperties=false on that new object before returning/using it. Locate
the usage of schemaConverter and the jsonSchema variable in
responses-tool-converter.ts and replace the in-place mutation with constructing
a new schema object (e.g., via object spread or a deep clone helper) then set
additionalProperties on that new object so the original converter output is not
modified.

In `@packages/typescript/openai-base/src/tools/computer-use-tool.ts`:
- Around line 23-31: Function signature inconsistent: change the parameter of
computerUseTool from ComputerUseTool to Omit<ComputerUseTool, 'type'> so callers
don't have to pass type. Update the factory function computerUseTool to accept
toolData: Omit<ComputerUseTool, 'type'>, keep returning the same object with
name: 'computer_use_preview' and metadata: { ...toolData }, and ensure any call
sites that previously passed type no longer do so (or remove the redundant type
property).

In `@packages/typescript/openai-base/src/tools/custom-tool.ts`:
- Around line 9-17: The convertCustomToolToAdapterFormat function currently
casts tool.metadata to CustomTool without runtime checks; add a Zod schema for
the CustomTool metadata, validate tool.metadata at the start of
convertCustomToolToAdapterFormat (using schema.parse or safeParse), and handle
validation failures by throwing or logging a descriptive error so malformed
metadata doesn't propagate; reference the convertCustomToolToAdapterFormat
function, the metadata variable and the CustomTool shape when adding the Zod
schema and validation call.

In `@packages/typescript/openai-base/src/tools/file-search-tool.ts`:
- Around line 15-26: convertFileSearchToolToAdapterFormat currently casts
tool.metadata to OpenAI.Responses.FileSearchTool without validation; add a
runtime type guard or validation at the start of
convertFileSearchToolToAdapterFormat to verify required fields (e.g.,
vector_store_ids, max_num_results, ranking_options, filters) and that
max_num_results meets the same constraints enforced by fileSearchTool, and throw
or return a clear error if validation fails so malformed metadata cannot produce
invalid adapter requests.

In `@packages/typescript/openai-base/src/tools/tool-converter.ts`:
- Around line 16-28: The SPECIAL_TOOL_NAMES set and the switch in the tool
conversion logic must be kept in sync to avoid falling back to
functionToolConverter incorrectly; add a defensive exhaustiveness guard by
updating the switch that branches on tool names (the same switch that currently
falls through to functionToolConverter) to include a default branch that throws
or logs an error referencing the unexpected tool name, and also add a runtime
check that every entry in SPECIAL_TOOL_NAMES is covered by a corresponding
switch case (or vice versa) so any drift fails fast during development.

In `@packages/typescript/openai-base/src/tools/web-search-tool.ts`:
- Around line 9-12: convertWebSearchToolToAdapterFormat currently casts
tool.metadata to WebSearchTool without validation; add defensive checks to
ensure tool.metadata exists and has the expected fields before returning it. In
convertWebSearchToolToAdapterFormat, validate that tool.metadata is not
undefined/null and that required WebSearchTool properties (e.g., any unique keys
your type requires) are present and correctly typed, and either throw a clear
error or return a safe default/converted object when validation fails; reference
the Tool type, WebSearchTool type, and the metadata variable to locate where to
add these checks.

In `@packages/typescript/openai-base/tests/schema-converter.test.ts`:
- Around line 32-45: Add a test to verify makeStructuredOutputCompatible
preserves existing nullable types: create a schema where one property already
has type ['string','null'] (e.g., nickname) and required includes only name,
call makeStructuredOutputCompatible(schema, ['name']) and assert that
nickname.type remains ['string','null'] (and name stays 'string'); reference the
existing test in schema-converter.test.ts and the makeStructuredOutputCompatible
function to place the new case next to the current optional-fields test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment on lines +75 to +81
try {
const stream = await this.client.chat.completions.create({
...requestParams,
stream: true,
})

yield* this.processStreamChunks(stream, options, aguiState)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find packages/typescript -name "chat-completions-text.ts" -type f

Repository: TanStack/ai

Length of output: 124


🏁 Script executed:

wc -l packages/typescript/openai-base/src/adapters/chat-completions-text.ts

Repository: TanStack/ai

Length of output: 128


🏁 Script executed:

cat -n packages/typescript/openai-base/src/adapters/chat-completions-text.ts

Repository: TanStack/ai

Length of output: 22122


🏁 Script executed:

rg -t ts "type TextOptions" packages/typescript --max-count 5

Repository: TanStack/ai

Length of output: 37


🏁 Script executed:

fd -t ts "\.ts$" packages/typescript -type f | xargs grep -l "export.*type TextOptions" | head -5

Repository: TanStack/ai

Length of output: 224


🏁 Script executed:

rg -t ts "export.*TextOptions" packages/typescript/ai/src --max-count 10

Repository: TanStack/ai

Length of output: 120


🏁 Script executed:

cat -n packages/typescript/ai/src/types.ts | head -150

Repository: TanStack/ai

Length of output: 5642


🏁 Script executed:

rg -A 20 "export interface TextOptions" packages/typescript/ai/src/types.ts

Repository: TanStack/ai

Length of output: 910


🏁 Script executed:

rg -A 60 "export interface TextOptions" packages/typescript/ai/src/types.ts

Repository: TanStack/ai

Length of output: 2504


🏁 Script executed:

rg "request\?" packages/typescript/ai/src/types.ts -B 2 -A 2

Repository: TanStack/ai

Length of output: 170


🏁 Script executed:

rg -A 20 "export interface StructuredOutputOptions" packages/typescript/ai/src

Repository: TanStack/ai

Length of output: 1856


🏁 Script executed:

rg -A 20 "export interface StructuredOutputOptions" packages/typescript/adapters/src

Repository: TanStack/ai

Length of output: 127


🏁 Script executed:

rg -B 5 "request\?: Request" packages/typescript/ai/src/types.ts

Repository: TanStack/ai

Length of output: 325


🏁 Script executed:

rg "chat\.completions\.create" packages/typescript/openai-base/src -A 5

Repository: TanStack/ai

Length of output: 2105


🏁 Script executed:

rg "client.chat.completions.create" packages/typescript --max-count 10 -A 3

Repository: TanStack/ai

Length of output: 2082


🏁 Script executed:

rg "mapOptionsToRequest" packages/typescript/openai-base/src/adapters/chat-completions-text.ts -A 30 -B 2

Repository: TanStack/ai

Length of output: 3029


🏁 Script executed:

rg "options.request\|chatOptions.request" packages/typescript/openai-base/src

Repository: TanStack/ai

Length of output: 37


🏁 Script executed:

rg "options\.request\|chatOptions\.request" packages/typescript --max-count 10

Repository: TanStack/ai

Length of output: 37


🏁 Script executed:

fd "openai" packages/typescript -type d

Repository: TanStack/ai

Length of output: 225


🏁 Script executed:

cat packages/typescript/openai-base/package.json | grep -A 5 -B 5 '"openai"'

Repository: TanStack/ai

Length of output: 388


🏁 Script executed:

find packages/typescript -name "package.json" -exec grep -l '"openai"' {} \;

Repository: TanStack/ai

Length of output: 183


🏁 Script executed:

cat packages/typescript/ai-groq/src/adapters/text.ts | head -200 | tail -100

Repository: TanStack/ai

Length of output: 3097


🏁 Script executed:

rg "\.request" packages/typescript/ai/src/types.ts -B 3 -A 3

Repository: TanStack/ai

Length of output: 37


🏁 Script executed:

web_search
OpenAI Node SDK chat.completions.create second argument RequestOptions headers

Repository: TanStack/ai

Length of output: 150


🏁 Script executed:

rg "request\?" packages/typescript/ai/src/types.ts -B 5 -A 5

Repository: TanStack/ai

Length of output: 507


🌐 Web query:

OpenAI Node.js SDK chat.completions.create RequestOptions second argument

💡 Result:

In the OpenAI Node.js SDK (latest versions as of 2026), the client.chat.completions.create method has the signature: client.chat.completions.create(ChatCompletionCreateParams body, RequestOptions? options): ChatCompletion | Stream The first argument is the ChatCompletionCreateParams object containing parameters like model, messages, temperature, etc. The second (optional) argument is RequestOptions options, which allows customizing the HTTP request. It typically includes properties for advanced request configuration such as custom headers, query parameters, timeout settings, or fetch options (e.g., proxy configuration, custom fetch implementation). This is part of the SDK's low-level request customization, generated from the OpenAPI spec using Stainless. Examples from official docs show basic usage with only the first argument, but the TypeScript reference explicitly documents the two-argument form. Usage example: const completion = await client.chat.completions.create( { model: 'gpt-4o', messages: [{ role: 'user', content: 'Hello!' }] }, { // RequestOptions headers: { 'Custom-Header': 'value' }, // other options like timeout, etc. } ); RequestOptions is an alias for Opts.RequestOptions in the SDK types, used across resource methods for request-level overrides.

Citations:


🏁 Script executed:

rg -A 50 "protected mapOptionsToRequest" packages/typescript/openai-base/src/adapters/chat-completions-text.ts

Repository: TanStack/ai

Length of output: 1607


🏁 Script executed:

rg "options\.request\|chatOptions" packages/typescript/openai-base/src/adapters/chat-completions-text.ts

Repository: TanStack/ai

Length of output: 37


🏁 Script executed:

cat -n packages/typescript/openai-base/src/adapters/chat-completions-text.ts | sed -n '61,82p'

Repository: TanStack/ai

Length of output: 870


🏁 Script executed:

cat -n packages/typescript/openai-base/src/adapters/chat-completions-text.ts | sed -n '129,153p'

Repository: TanStack/ai

Length of output: 936


Forward request-scoped headers through both Chat Completions calls.

The OpenAI SDK's chat.completions.create() method accepts a second argument for RequestOptions, which includes custom headers and other request configuration. The TextOptions interface provides a request?: Request | RequestInit field for this purpose, but it's never extracted or passed to either the streaming call (line 76) or structured-output call (line 141). Update mapOptionsToRequest() to return both the request parameters and the request options, then pass them as the second argument to both create() invocations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/adapters/chat-completions-text.ts` around
lines 75 - 81, mapOptionsToRequest currently only returns request parameters and
ignores the optional request/requestInit in TextOptions; update
mapOptionsToRequest to return a tuple or object containing both the
requestParams and requestOptions (derived from TextOptions.request), then change
both calls to this.client.chat.completions.create(...) (the streaming call that
feeds processStreamChunks and the structured-output create call) to pass the
requestOptions as the second argument to create(). Ensure requestOptions is the
same forwarded Request | RequestInit so per-request headers and other
RequestOptions are preserved.

Comment on lines +12 to +24
export function makeStructuredOutputCompatible(
schema: Record<string, any>,
originalRequired: Array<string> = [],
): Record<string, any> {
const result = { ...schema }

if (result.type === 'object' && result.properties) {
const properties = { ...result.properties }
const allPropertyNames = Object.keys(properties)

for (const propName of allPropertyNames) {
const prop = properties[propName]
const wasOptional = !originalRequired.includes(propName)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use schema.required as the default source of optionality.

With originalRequired = [], a one-arg call like makeStructuredOutputCompatible(schema) treats every root property as optional. Line 24 then widens even originally required fields to include null, which changes the schema contract before required is rewritten.

🛠️ Suggested fix
 export function makeStructuredOutputCompatible(
   schema: Record<string, any>,
-  originalRequired: Array<string> = [],
+  originalRequired?: Array<string>,
 ): Record<string, any> {
   const result = { ...schema }
+  const required =
+    originalRequired ??
+    (Array.isArray(result.required) ? result.required : [])
-      const wasOptional = !originalRequired.includes(propName)
+      const wasOptional = !required.includes(propName)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function makeStructuredOutputCompatible(
schema: Record<string, any>,
originalRequired: Array<string> = [],
): Record<string, any> {
const result = { ...schema }
if (result.type === 'object' && result.properties) {
const properties = { ...result.properties }
const allPropertyNames = Object.keys(properties)
for (const propName of allPropertyNames) {
const prop = properties[propName]
const wasOptional = !originalRequired.includes(propName)
export function makeStructuredOutputCompatible(
schema: Record<string, any>,
originalRequired?: Array<string>,
): Record<string, any> {
const result = { ...schema }
const required =
originalRequired ??
(Array.isArray(result.required) ? result.required : [])
if (result.type === 'object' && result.properties) {
const properties = { ...result.properties }
const allPropertyNames = Object.keys(properties)
for (const propName of allPropertyNames) {
const prop = properties[propName]
const wasOptional = !required.includes(propName)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/utils/schema-converter.ts` around lines
12 - 24, The function makeStructuredOutputCompatible currently treats all fields
as optional when originalRequired defaults to [], which can incorrectly widen
originally required properties; change the optionality source to use
schema.required when originalRequired is not supplied—inside
makeStructuredOutputCompatible, derive a working required list (e.g., const
requiredSource = originalRequired.length ? originalRequired : (schema.required
?? [])) and use requiredSource instead of originalRequired when computing
wasOptional for each propName, ensuring originally required fields are not
widened to include null before result.required is rewritten.

Introduces the @tanstack/ai-utils package providing shared, zero-dependency utilities (generateId, getApiKeyFromEnv, transformNullsToUndefined, defineModelMeta) for use across TanStack AI adapter packages.
- Fix getApiKeyFromEnv to check globalThis.window.env before process.env, matching all existing adapters
- Fix generateId to use substring(7) instead of substring(2,10) for consistent random part length
- Add JSDoc to transformNullsToUndefined explaining the null→undefined JSON Schema reason
- Add missing test cases for empty output modalities and negative output pricing
Extract the streaming Chat Completions pipeline from ai-grok into a
reusable OpenAICompatibleChatCompletionsTextAdapter. Providers that use
the OpenAI Chat Completions API (/v1/chat/completions) can extend this
class and only need to set baseURL, lock type parameters, and override
methods for provider-specific quirks.

Protected override points: mapOptionsToRequest, convertMessage,
convertContentPart, processStreamChunks, makeStructuredOutputCompatible,
normalizeContent, extractTextContent.

Also adds Chat Completions-specific tool converter (distinct from the
existing Responses API tool converter).
Extract and generalize the OpenAI Responses API text adapter into
OpenAICompatibleResponsesTextAdapter. This handles the full Responses
API streaming pipeline (9+ event types), including reasoning/thinking
tokens, tool call streaming, and structured output via text.format.

Also adds responses-tool-converter for the flat Responses API tool
format (distinct from Chat Completions' nested function format).
…ai-utils

Migrate ai-openai to extend base classes from openai-base and delegate
utility functions to ai-utils, eliminating ~1,800 lines of duplicated
code while maintaining zero breaking changes.

Changes:
- utils/client.ts: generateId and getOpenAIApiKeyFromEnv delegate to ai-utils
- utils/schema-converter.ts: transformNullsToUndefined and makeOpenAIStructuredOutputCompatible delegate to ai-utils/openai-base
- tools/*: all 14 tool files re-export from openai-base
- adapters/text.ts: extends OpenAICompatibleResponsesTextAdapter, overrides mapOptionsToRequest for OpenAI-specific tool conversion and validation
- adapters/image.ts: extends OpenAICompatibleImageAdapter, overrides validation methods
- adapters/summarize.ts: extends OpenAICompatibleSummarizeAdapter
- adapters/tts.ts: extends OpenAICompatibleTTSAdapter, overrides validation
- adapters/transcription.ts: extends OpenAICompatibleTranscriptionAdapter
- adapters/video.ts: extends OpenAICompatibleVideoAdapter, overrides validation and request building
- openai-base config.ts: removed explicit baseURL to avoid null incompatibility

All 127 existing tests pass, types check clean, build succeeds.
Replace duplicated generateId, env-var lookup, and null-transform logic
in ai-groq, ai-openrouter, and ai-ollama with imports from @tanstack/ai-utils
and @tanstack/openai-base. makeGroqStructuredOutputCompatible now delegates
to makeStructuredOutputCompatible and applies the Groq-specific quirk of
removing empty required arrays.
…tanstack/ai-utils

Replace duplicated generateId and getXxxApiKeyFromEnv implementations in
ai-anthropic, ai-gemini, ai-fal, and ai-elevenlabs with imports from
@tanstack/ai-utils. All provider-specific wrapper function names preserved
for backwards compatibility.
@AlemTuzlak AlemTuzlak force-pushed the feat/extract-openai-base-and-ai-utils branch from ce49ae5 to 914b883 Compare March 30, 2026 16:33
- Fix schema-converter default required parameter and null-widening for nested types
- Fix removeEmptyRequired to recurse into anyOf/oneOf/allOf/additionalProperties (groq)
- Forward modelOptions, request headers/signal in chat-completions-text adapter
- Remove stream_options leak into non-streaming structured output calls
- Use call_id instead of internal id for tool call correlation (responses-text)
- Make transcription verbose_json default provider-agnostic via protected override
- Add runtime guards for ArrayBuffer and atob in transcription adapter
- Derive TTS outputFormat from merged request after modelOptions spread
- Add downloadContent probe and fix expires_at seconds-to-milliseconds (video)
- Fix mcp-tool type ordering so metadata cannot override type: 'mcp'
- Add tests proving all fixes work
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
packages/typescript/openai-base/tests/mcp-tool.test.ts (1)

20-33: Align the preservation assertion with the test’s stated intent.

The test says it preserves metadata fields other than type, but it only asserts server_url. Add an assertion for server_description (or assert the full projected object) to avoid false confidence.

Suggested diff
   it('should preserve metadata fields other than type', () => {
@@
     const result = convertMCPToolToAdapterFormat(tool)
     expect(result.type).toBe('mcp')
     expect(result.server_url).toBe('https://example.com/mcp')
+    expect(result.server_description).toBe('Test server')
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/tests/mcp-tool.test.ts` around lines 20 - 33,
The test currently only asserts that convertMCPToolToAdapterFormat(tool)
preserves result.server_url even though it’s intended to preserve all metadata
fields other than type; update the assertion to also check
result.server_description (or assert the full projected object equals the
expected projection) so the test verifies both server_url and server_description
are preserved from the input tool.metadata when calling
convertMCPToolToAdapterFormat.
packages/typescript/openai-base/src/types/provider-options.ts (1)

7-7: Use Array<string> per ESLint configuration.

Static analysis indicates the project prefers Array<string> over string[].

✏️ Suggested fix
-  stop?: string | string[]
+  stop?: string | Array<string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/types/provider-options.ts` at line 7, The
stop property is declared using the `string[]` shorthand; update its type to use
`Array<string>` to satisfy the project's ESLint rule. Locate the `stop` property
in the provider options type (the `stop?: string | string[]` declaration in
provider-options.ts) and change the union to use `Array<string>` (e.g., `stop?:
string | Array<string>`) so the signature remains identical but conforms to the
Array<string> style.
packages/typescript/ai-grok/src/utils/client.ts (1)

30-33: Consider avoiding the double cast through unknown.

The as unknown as OpenAICompatibleClientConfig pattern bypasses type safety. If GrokClientConfig (which extends ClientOptions) is structurally compatible with OpenAICompatibleClientConfig, consider either:

  1. Aligning the types so direct assignment works
  2. Using a type assertion with a comment explaining why it's safe

This isn't blocking since the runtime behavior is correct, but the double cast hides potential type mismatches.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-grok/src/utils/client.ts` around lines 30 - 33, The
return currently uses a double cast "as unknown as OpenAICompatibleClientConfig"
which bypasses type safety; update the types or the cast so we don't go through
unknown: either make GrokClientConfig (or ClientOptions) structurally extend
OpenAICompatibleClientConfig so the object returned by the function can be
returned directly, or replace the double-cast with a single assert "as
OpenAICompatibleClientConfig" and add a short comment above the return
explaining why the shape is compatible (refer to the returned object created by
the spread of config and baseURL in client.ts). Ensure the change touches the
return expression that builds the config object so the type assertion is safe
and clear.
packages/typescript/openai-base/src/adapters/chat-completions-text.ts (1)

413-428: Use console.error for error logging consistency.

Line 415 uses console.log for error logging, while the rest of the file uses console.error for errors (lines 117-122, 200-203). This inconsistency could make log filtering more difficult.

♻️ Suggested fix
     } catch (error: unknown) {
       const err = error as Error & { code?: string }
-      console.log(`[${this.name}] Stream ended with error:`, err.message)
+      console.error(`[${this.name}] Stream ended with error:`, err.message)
 
       // Emit AG-UI RUN_ERROR
       yield {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/openai-base/src/adapters/chat-completions-text.ts` around
lines 413 - 428, Replace the inconsistent console.log call in the catch block
with console.error so error logging matches the rest of the file; specifically
in the catch (error: unknown) block where you cast to const err = error as Error
& { code?: string } and log `[${this.name}] Stream ended with error:`, change
that call to use console.error and keep the rest of the RUN_ERROR yield (runId,
model, timestamp, error.message, error.code) unchanged.
packages/typescript/ai-grok/src/adapters/text.ts (1)

27-41: Successful refactor to use shared base adapter.

The adapter correctly extends OpenAICompatibleChatCompletionsTextAdapter, eliminating ~500 lines of duplicated streaming and structured output logic.

Nit: Lines 35-36 declare kind and name as class properties, but these are already set by the parent class (via constructor parameter and parent's kind declaration). Consider removing the redundant declarations to clarify that values come from the inheritance chain.

♻️ Optional: Remove redundant property declarations
 export class GrokTextAdapter<
   TModel extends (typeof GROK_CHAT_MODELS)[number],
 > extends OpenAICompatibleChatCompletionsTextAdapter<
   TModel,
   ResolveProviderOptions<TModel>,
   ResolveInputModalities<TModel>,
   GrokMessageMetadataByModality
 > {
-  readonly kind = 'text' as const
-  readonly name = 'grok' as const
+  override readonly name = 'grok' as const

   constructor(config: GrokTextConfig, model: TModel) {
     super(toCompatibleConfig(config), model, 'grok')
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-grok/src/adapters/text.ts` around lines 27 - 41,
Remove the redundant class properties readonly kind and readonly name from
GrokTextAdapter since the parent OpenAICompatibleChatCompletionsTextAdapter
already sets these via its constructor parameter (the 'grok' argument passed in
the GrokTextAdapter constructor); delete the two property declarations (kind and
name) so the adapter inherits them from the base class and ensure no other code
in this class relies on those explicit declarations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/typescript/ai-groq/src/utils/client.ts`:
- Line 3: The import order violates ESLint import/order: move the concrete
import of generateId and getApiKeyFromEnv from '@tanstack/ai-utils' so it comes
before the `import type` from 'groq-sdk'; update the top of the file so the
non-type import (generateId as _generateId, getApiKeyFromEnv) appears before the
`import type { ... } from 'groq-sdk'` to satisfy the rule.

In `@packages/typescript/ai-groq/tests/schema-converter.test.ts`:
- Around line 32-64: Rename the test case title to reflect the actual behavior
being tested: update the it(...) description string in the test where
makeGroqStructuredOutputCompatible and checkNoEmptyRequired are used (currently
"'should remove empty required arrays inside oneOf variants'") to a clearer name
such as "'should not have empty required arrays anywhere in the result'"; ensure
only the descriptive string is changed and leave the test body (the schema, call
to makeGroqStructuredOutputCompatible, and the checkNoEmptyRequired helper)
untouched.

In `@packages/typescript/ai-utils/tests/env.test.ts`:
- Around line 14-16: The test is flaky because it relies on host environment
state; make the assertions deterministic by explicitly controlling process.env
within the test: save the original value of the env var, delete it (delete
process.env['NONEXISTENT_KEY']) before calling
getApiKeyFromEnv('NONEXISTENT_KEY') so the function reliably throws, and then
restore the original value after the test (or use beforeEach/afterEach). Do the
same for the other test that references 'MY_PROVIDER_API_KEY' — set
process.env['MY_PROVIDER_API_KEY'] to a known value for the success case and
restore the original afterward; reference getApiKeyFromEnv in the test file to
locate the tests to change.

---

Nitpick comments:
In `@packages/typescript/ai-grok/src/adapters/text.ts`:
- Around line 27-41: Remove the redundant class properties readonly kind and
readonly name from GrokTextAdapter since the parent
OpenAICompatibleChatCompletionsTextAdapter already sets these via its
constructor parameter (the 'grok' argument passed in the GrokTextAdapter
constructor); delete the two property declarations (kind and name) so the
adapter inherits them from the base class and ensure no other code in this class
relies on those explicit declarations.

In `@packages/typescript/ai-grok/src/utils/client.ts`:
- Around line 30-33: The return currently uses a double cast "as unknown as
OpenAICompatibleClientConfig" which bypasses type safety; update the types or
the cast so we don't go through unknown: either make GrokClientConfig (or
ClientOptions) structurally extend OpenAICompatibleClientConfig so the object
returned by the function can be returned directly, or replace the double-cast
with a single assert "as OpenAICompatibleClientConfig" and add a short comment
above the return explaining why the shape is compatible (refer to the returned
object created by the spread of config and baseURL in client.ts). Ensure the
change touches the return expression that builds the config object so the type
assertion is safe and clear.

In `@packages/typescript/openai-base/src/adapters/chat-completions-text.ts`:
- Around line 413-428: Replace the inconsistent console.log call in the catch
block with console.error so error logging matches the rest of the file;
specifically in the catch (error: unknown) block where you cast to const err =
error as Error & { code?: string } and log `[${this.name}] Stream ended with
error:`, change that call to use console.error and keep the rest of the
RUN_ERROR yield (runId, model, timestamp, error.message, error.code) unchanged.

In `@packages/typescript/openai-base/src/types/provider-options.ts`:
- Line 7: The stop property is declared using the `string[]` shorthand; update
its type to use `Array<string>` to satisfy the project's ESLint rule. Locate the
`stop` property in the provider options type (the `stop?: string | string[]`
declaration in provider-options.ts) and change the union to use `Array<string>`
(e.g., `stop?: string | Array<string>`) so the signature remains identical but
conforms to the Array<string> style.

In `@packages/typescript/openai-base/tests/mcp-tool.test.ts`:
- Around line 20-33: The test currently only asserts that
convertMCPToolToAdapterFormat(tool) preserves result.server_url even though it’s
intended to preserve all metadata fields other than type; update the assertion
to also check result.server_description (or assert the full projected object
equals the expected projection) so the test verifies both server_url and
server_description are preserved from the input tool.metadata when calling
convertMCPToolToAdapterFormat.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a438aa99-181f-4c47-8e75-5a71a52dae2b

📥 Commits

Reviewing files that changed from the base of the PR and between ce49ae5 and 9b83f1a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (104)
  • .changeset/add-ai-utils-package.md
  • .changeset/add-openai-base-package.md
  • .changeset/refactor-providers-to-shared-packages.md
  • packages/typescript/ai-anthropic/package.json
  • packages/typescript/ai-anthropic/src/utils/client.ts
  • packages/typescript/ai-elevenlabs/package.json
  • packages/typescript/ai-elevenlabs/src/realtime/token.ts
  • packages/typescript/ai-fal/package.json
  • packages/typescript/ai-fal/src/utils/client.ts
  • packages/typescript/ai-gemini/package.json
  • packages/typescript/ai-gemini/src/utils/client.ts
  • packages/typescript/ai-grok/package.json
  • packages/typescript/ai-grok/src/adapters/image.ts
  • packages/typescript/ai-grok/src/adapters/summarize.ts
  • packages/typescript/ai-grok/src/adapters/text.ts
  • packages/typescript/ai-grok/src/tools/function-tool.ts
  • packages/typescript/ai-grok/src/tools/tool-converter.ts
  • packages/typescript/ai-grok/src/utils/client.ts
  • packages/typescript/ai-grok/src/utils/index.ts
  • packages/typescript/ai-grok/src/utils/schema-converter.ts
  • packages/typescript/ai-grok/tests/grok-adapter.test.ts
  • packages/typescript/ai-groq/package.json
  • packages/typescript/ai-groq/src/utils/client.ts
  • packages/typescript/ai-groq/src/utils/schema-converter.ts
  • packages/typescript/ai-groq/tests/groq-adapter.test.ts
  • packages/typescript/ai-groq/tests/schema-converter.test.ts
  • packages/typescript/ai-ollama/package.json
  • packages/typescript/ai-ollama/src/utils/client.ts
  • packages/typescript/ai-openai/package.json
  • packages/typescript/ai-openai/src/adapters/image.ts
  • packages/typescript/ai-openai/src/adapters/summarize.ts
  • packages/typescript/ai-openai/src/adapters/text.ts
  • packages/typescript/ai-openai/src/adapters/transcription.ts
  • packages/typescript/ai-openai/src/adapters/tts.ts
  • packages/typescript/ai-openai/src/adapters/video.ts
  • packages/typescript/ai-openai/src/tools/apply-patch-tool.ts
  • packages/typescript/ai-openai/src/tools/code-interpreter-tool.ts
  • packages/typescript/ai-openai/src/tools/computer-use-tool.ts
  • packages/typescript/ai-openai/src/tools/custom-tool.ts
  • packages/typescript/ai-openai/src/tools/file-search-tool.ts
  • packages/typescript/ai-openai/src/tools/function-tool.ts
  • packages/typescript/ai-openai/src/tools/image-generation-tool.ts
  • packages/typescript/ai-openai/src/tools/index.ts
  • packages/typescript/ai-openai/src/tools/local-shell-tool.ts
  • packages/typescript/ai-openai/src/tools/mcp-tool.ts
  • packages/typescript/ai-openai/src/tools/shell-tool.ts
  • packages/typescript/ai-openai/src/tools/tool-choice.ts
  • packages/typescript/ai-openai/src/tools/tool-converter.ts
  • packages/typescript/ai-openai/src/tools/web-search-preview-tool.ts
  • packages/typescript/ai-openai/src/tools/web-search-tool.ts
  • packages/typescript/ai-openai/src/utils/client.ts
  • packages/typescript/ai-openai/src/utils/schema-converter.ts
  • packages/typescript/ai-openrouter/package.json
  • packages/typescript/ai-openrouter/src/utils/client.ts
  • packages/typescript/ai-utils/package.json
  • packages/typescript/ai-utils/src/env.ts
  • packages/typescript/ai-utils/src/id.ts
  • packages/typescript/ai-utils/src/index.ts
  • packages/typescript/ai-utils/src/model-meta/define.ts
  • packages/typescript/ai-utils/src/model-meta/types.ts
  • packages/typescript/ai-utils/src/transforms.ts
  • packages/typescript/ai-utils/tests/env.test.ts
  • packages/typescript/ai-utils/tests/id.test.ts
  • packages/typescript/ai-utils/tests/model-meta.test.ts
  • packages/typescript/ai-utils/tests/transforms.test.ts
  • packages/typescript/ai-utils/tsconfig.json
  • packages/typescript/ai-utils/vite.config.ts
  • packages/typescript/openai-base/package.json
  • packages/typescript/openai-base/src/adapters/chat-completions-text.ts
  • packages/typescript/openai-base/src/adapters/chat-completions-tool-converter.ts
  • packages/typescript/openai-base/src/adapters/image.ts
  • packages/typescript/openai-base/src/adapters/responses-text.ts
  • packages/typescript/openai-base/src/adapters/responses-tool-converter.ts
  • packages/typescript/openai-base/src/adapters/summarize.ts
  • packages/typescript/openai-base/src/adapters/transcription.ts
  • packages/typescript/openai-base/src/adapters/tts.ts
  • packages/typescript/openai-base/src/adapters/video.ts
  • packages/typescript/openai-base/src/index.ts
  • packages/typescript/openai-base/src/tools/apply-patch-tool.ts
  • packages/typescript/openai-base/src/tools/code-interpreter-tool.ts
  • packages/typescript/openai-base/src/tools/computer-use-tool.ts
  • packages/typescript/openai-base/src/tools/custom-tool.ts
  • packages/typescript/openai-base/src/tools/file-search-tool.ts
  • packages/typescript/openai-base/src/tools/function-tool.ts
  • packages/typescript/openai-base/src/tools/image-generation-tool.ts
  • packages/typescript/openai-base/src/tools/index.ts
  • packages/typescript/openai-base/src/tools/local-shell-tool.ts
  • packages/typescript/openai-base/src/tools/mcp-tool.ts
  • packages/typescript/openai-base/src/tools/shell-tool.ts
  • packages/typescript/openai-base/src/tools/tool-choice.ts
  • packages/typescript/openai-base/src/tools/tool-converter.ts
  • packages/typescript/openai-base/src/tools/web-search-preview-tool.ts
  • packages/typescript/openai-base/src/tools/web-search-tool.ts
  • packages/typescript/openai-base/src/types/config.ts
  • packages/typescript/openai-base/src/types/message-metadata.ts
  • packages/typescript/openai-base/src/types/provider-options.ts
  • packages/typescript/openai-base/src/utils/client.ts
  • packages/typescript/openai-base/src/utils/schema-converter.ts
  • packages/typescript/openai-base/tests/chat-completions-text.test.ts
  • packages/typescript/openai-base/tests/mcp-tool.test.ts
  • packages/typescript/openai-base/tests/responses-text.test.ts
  • packages/typescript/openai-base/tests/schema-converter.test.ts
  • packages/typescript/openai-base/tsconfig.json
  • packages/typescript/openai-base/vite.config.ts
✅ Files skipped from review due to trivial changes (36)
  • packages/typescript/ai-ollama/package.json
  • packages/typescript/ai-fal/package.json
  • packages/typescript/ai-groq/package.json
  • packages/typescript/ai-utils/tests/id.test.ts
  • packages/typescript/ai-elevenlabs/package.json
  • packages/typescript/ai-utils/tsconfig.json
  • packages/typescript/ai-elevenlabs/src/realtime/token.ts
  • packages/typescript/ai-openai/package.json
  • packages/typescript/openai-base/tsconfig.json
  • packages/typescript/openai-base/src/utils/client.ts
  • packages/typescript/ai-anthropic/package.json
  • packages/typescript/ai-grok/package.json
  • packages/typescript/ai-gemini/package.json
  • packages/typescript/ai-openai/src/tools/tool-choice.ts
  • packages/typescript/ai-openrouter/package.json
  • packages/typescript/openai-base/src/tools/web-search-preview-tool.ts
  • packages/typescript/openai-base/src/types/config.ts
  • packages/typescript/ai-utils/src/id.ts
  • .changeset/add-ai-utils-package.md
  • packages/typescript/openai-base/src/tools/local-shell-tool.ts
  • packages/typescript/openai-base/src/tools/apply-patch-tool.ts
  • packages/typescript/openai-base/src/tools/tool-converter.ts
  • packages/typescript/ai-grok/src/utils/index.ts
  • packages/typescript/ai-utils/src/index.ts
  • packages/typescript/openai-base/src/tools/code-interpreter-tool.ts
  • packages/typescript/ai-openai/src/tools/local-shell-tool.ts
  • packages/typescript/ai-openai/src/tools/custom-tool.ts
  • packages/typescript/ai-utils/tests/model-meta.test.ts
  • packages/typescript/openai-base/package.json
  • packages/typescript/openai-base/src/tools/shell-tool.ts
  • packages/typescript/openai-base/src/tools/tool-choice.ts
  • packages/typescript/ai-utils/src/model-meta/types.ts
  • packages/typescript/openai-base/src/types/message-metadata.ts
  • packages/typescript/openai-base/src/adapters/tts.ts
  • packages/typescript/openai-base/src/index.ts
  • packages/typescript/openai-base/src/adapters/responses-text.ts
🚧 Files skipped from review as they are similar to previous changes (41)
  • packages/typescript/ai-ollama/src/utils/client.ts
  • .changeset/add-openai-base-package.md
  • packages/typescript/ai-utils/src/transforms.ts
  • packages/typescript/ai-openai/src/tools/function-tool.ts
  • packages/typescript/ai-openai/src/tools/index.ts
  • packages/typescript/ai-utils/tests/transforms.test.ts
  • packages/typescript/openai-base/src/tools/web-search-tool.ts
  • packages/typescript/openai-base/src/tools/function-tool.ts
  • packages/typescript/ai-grok/src/tools/tool-converter.ts
  • packages/typescript/ai-groq/tests/groq-adapter.test.ts
  • packages/typescript/ai-openai/src/tools/apply-patch-tool.ts
  • packages/typescript/ai-anthropic/src/utils/client.ts
  • .changeset/refactor-providers-to-shared-packages.md
  • packages/typescript/ai-grok/src/adapters/summarize.ts
  • packages/typescript/ai-utils/vite.config.ts
  • packages/typescript/ai-openai/src/tools/shell-tool.ts
  • packages/typescript/openai-base/src/tools/image-generation-tool.ts
  • packages/typescript/ai-openai/src/tools/computer-use-tool.ts
  • packages/typescript/ai-openai/src/tools/mcp-tool.ts
  • packages/typescript/ai-openai/src/tools/file-search-tool.ts
  • packages/typescript/ai-utils/src/model-meta/define.ts
  • packages/typescript/openai-base/src/tools/mcp-tool.ts
  • packages/typescript/openai-base/src/tools/computer-use-tool.ts
  • packages/typescript/openai-base/src/tools/custom-tool.ts
  • packages/typescript/ai-utils/src/env.ts
  • packages/typescript/ai-openai/src/tools/web-search-tool.ts
  • packages/typescript/ai-openai/src/tools/code-interpreter-tool.ts
  • packages/typescript/openai-base/src/adapters/responses-tool-converter.ts
  • packages/typescript/ai-openai/src/utils/client.ts
  • packages/typescript/ai-utils/package.json
  • packages/typescript/openai-base/src/tools/file-search-tool.ts
  • packages/typescript/ai-openai/src/utils/schema-converter.ts
  • packages/typescript/openai-base/src/adapters/chat-completions-tool-converter.ts
  • packages/typescript/openai-base/src/tools/index.ts
  • packages/typescript/openai-base/src/utils/schema-converter.ts
  • packages/typescript/openai-base/src/adapters/summarize.ts
  • packages/typescript/ai-gemini/src/utils/client.ts
  • packages/typescript/openai-base/src/adapters/image.ts
  • packages/typescript/ai-openrouter/src/utils/client.ts
  • packages/typescript/openai-base/vite.config.ts
  • packages/typescript/openai-base/tests/schema-converter.test.ts

Comment on lines +32 to +64
it('should remove empty required arrays inside oneOf variants', () => {
const schema = {
type: 'object',
properties: {
data: {
type: 'object',
properties: {
inner: { type: 'string' },
},
required: ['inner'],
},
},
required: ['data'],
}

// First create a schema that would produce empty required after processing
const result = makeGroqStructuredOutputCompatible(schema, ['data'])

// Should not have empty required arrays anywhere
const checkNoEmptyRequired = (obj: any): void => {
if (obj && typeof obj === 'object') {
if (Array.isArray(obj.required)) {
expect(obj.required.length).toBeGreaterThan(0)
}
for (const value of Object.values(obj)) {
if (typeof value === 'object' && value !== null) {
checkNoEmptyRequired(value)
}
}
}
}
checkNoEmptyRequired(result)
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Misleading test name: schema doesn't contain oneOf.

The test is named 'should remove empty required arrays inside oneOf variants' but the schema only contains a nested object structure without any oneOf array. Consider renaming to accurately describe what's being tested (e.g., 'should not have empty required arrays anywhere in the result').

✏️ Suggested fix
-  it('should remove empty required arrays inside oneOf variants', () => {
+  it('should not have any empty required arrays in nested structures', () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should remove empty required arrays inside oneOf variants', () => {
const schema = {
type: 'object',
properties: {
data: {
type: 'object',
properties: {
inner: { type: 'string' },
},
required: ['inner'],
},
},
required: ['data'],
}
// First create a schema that would produce empty required after processing
const result = makeGroqStructuredOutputCompatible(schema, ['data'])
// Should not have empty required arrays anywhere
const checkNoEmptyRequired = (obj: any): void => {
if (obj && typeof obj === 'object') {
if (Array.isArray(obj.required)) {
expect(obj.required.length).toBeGreaterThan(0)
}
for (const value of Object.values(obj)) {
if (typeof value === 'object' && value !== null) {
checkNoEmptyRequired(value)
}
}
}
}
checkNoEmptyRequired(result)
})
it('should not have any empty required arrays in nested structures', () => {
const schema = {
type: 'object',
properties: {
data: {
type: 'object',
properties: {
inner: { type: 'string' },
},
required: ['inner'],
},
},
required: ['data'],
}
// First create a schema that would produce empty required after processing
const result = makeGroqStructuredOutputCompatible(schema, ['data'])
// Should not have empty required arrays anywhere
const checkNoEmptyRequired = (obj: any): void => {
if (obj && typeof obj === 'object') {
if (Array.isArray(obj.required)) {
expect(obj.required.length).toBeGreaterThan(0)
}
for (const value of Object.values(obj)) {
if (typeof value === 'object' && value !== null) {
checkNoEmptyRequired(value)
}
}
}
}
checkNoEmptyRequired(result)
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-groq/tests/schema-converter.test.ts` around lines 32 -
64, Rename the test case title to reflect the actual behavior being tested:
update the it(...) description string in the test where
makeGroqStructuredOutputCompatible and checkNoEmptyRequired are used (currently
"'should remove empty required arrays inside oneOf variants'") to a clearer name
such as "'should not have empty required arrays anywhere in the result'"; ensure
only the descriptive string is changed and leave the test body (the schema, call
to makeGroqStructuredOutputCompatible, and the checkNoEmptyRequired helper)
untouched.

Comment on lines +14 to +16
it('should throw if the env var is not set', () => {
expect(() => getApiKeyFromEnv('NONEXISTENT_KEY')).toThrow('NONEXISTENT_KEY')
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make missing-env assertions deterministic to avoid host-env collisions.

These tests assume NONEXISTENT_KEY / MY_PROVIDER_API_KEY are absent in the runner environment. If either is present in CI, assertions can become flaky.

💡 Proposed patch
   it('should throw if the env var is not set', () => {
-    expect(() => getApiKeyFromEnv('NONEXISTENT_KEY')).toThrow('NONEXISTENT_KEY')
+    const missingKey = `__AI_UTILS_TEST_MISSING_${Date.now()}__`
+    expect(() => getApiKeyFromEnv(missingKey)).toThrow(missingKey)
   })
@@
   it('should include the env var name in the error message', () => {
-    expect(() => getApiKeyFromEnv('MY_PROVIDER_API_KEY')).toThrow(
-      'MY_PROVIDER_API_KEY',
-    )
+    const missingKey = `__AI_UTILS_TEST_MISSING_MSG_${Date.now()}__`
+    expect(() => getApiKeyFromEnv(missingKey)).toThrow(missingKey)
   })

Also applies to: 23-27

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-utils/tests/env.test.ts` around lines 14 - 16, The
test is flaky because it relies on host environment state; make the assertions
deterministic by explicitly controlling process.env within the test: save the
original value of the env var, delete it (delete process.env['NONEXISTENT_KEY'])
before calling getApiKeyFromEnv('NONEXISTENT_KEY') so the function reliably
throws, and then restore the original value after the test (or use
beforeEach/afterEach). Do the same for the other test that references
'MY_PROVIDER_API_KEY' — set process.env['MY_PROVIDER_API_KEY'] to a known value
for the success case and restore the original afterward; reference
getApiKeyFromEnv in the test file to locate the tests to change.

- Fix unnecessary type assertions in chat-completions-text and responses-text
- Fix eslint import order in ai-groq client.ts
- Fix unnecessary condition in ai-groq schema-converter combinator recursion
- Fix array-type lint error in openai-base provider-options
- Remove unused files in ai-grok (tools/index.ts, tool-converter.ts, function-tool.ts)
- Remove unused exports (createOpenAIClient, generateId, validateTextProviderOptions, InternalTextProviderOptions)
@AlemTuzlak AlemTuzlak force-pushed the feat/extract-openai-base-and-ai-utils branch from 9b83f1a to 012f2a1 Compare March 30, 2026 17:02
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/typescript/ai-groq/src/utils/schema-converter.ts`:
- Around line 82-93: The current normalization only patches a top-level `{ type:
'object' }` (variable normalised) and misses nested empty-object branches,
causing child schemas without properties to slip through
makeStructuredOutputCompatible; update schema-converter.ts to recursively walk
the schema before calling makeStructuredOutputCompatible and ensure any node
with `type === 'object'` but no `properties` gets `properties: {}` (handle
nested locations: `properties` values, `items`, `anyOf`/`oneOf`/`allOf`, and
`additionalProperties`), keep the existing call to removeEmptyRequired for final
cleanup, and add a regression test that constructs a schema like `{ type:
'object', properties: { child: { type: 'object' } }, required: ['child'] }` to
verify the nested child receives an empty properties object and passes Groq
validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a9d14e92-f334-464f-92c8-e0413dd4905f

📥 Commits

Reviewing files that changed from the base of the PR and between 9b83f1a and 19231f2.

📒 Files selected for processing (12)
  • packages/typescript/ai-grok/src/text/text-provider-options.ts
  • packages/typescript/ai-grok/src/tools/function-tool.ts
  • packages/typescript/ai-grok/src/tools/index.ts
  • packages/typescript/ai-grok/src/tools/tool-converter.ts
  • packages/typescript/ai-groq/src/utils/client.ts
  • packages/typescript/ai-groq/src/utils/schema-converter.ts
  • packages/typescript/ai-openai/src/utils/client.ts
  • packages/typescript/openai-base/src/adapters/chat-completions-text.ts
  • packages/typescript/openai-base/src/adapters/responses-text.ts
  • packages/typescript/openai-base/src/types/provider-options.ts
  • packages/typescript/openai-base/src/utils/schema-converter.ts
  • packages/typescript/openai-base/tests/chat-completions-text.test.ts
💤 Files with no reviewable changes (4)
  • packages/typescript/ai-grok/src/tools/index.ts
  • packages/typescript/ai-grok/src/text/text-provider-options.ts
  • packages/typescript/ai-grok/src/tools/function-tool.ts
  • packages/typescript/ai-grok/src/tools/tool-converter.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/typescript/openai-base/src/types/provider-options.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/typescript/ai-groq/src/utils/client.ts
  • packages/typescript/openai-base/src/utils/schema-converter.ts
  • packages/typescript/openai-base/tests/chat-completions-text.test.ts

Comment on lines +82 to +93
// Ensure object schemas always have properties (e.g. z.object({}) may produce
// { type: 'object' } without properties). openai-base's transformer skips
// objects without properties, so we normalise first.
const normalised =
schema.type === 'object' && !schema.properties
? { ...schema, properties: {} }
: schema

result.properties = properties
// Groq rejects `required` when there are no properties, even if it's an empty array
if (allPropertyNames.length > 0) {
result.required = allPropertyNames
} else {
delete result.required
}
result.additionalProperties = false
}
const result = makeStructuredOutputCompatible(normalised, originalRequired)

if (result.type === 'array' && result.items) {
result.items = makeGroqStructuredOutputCompatible(
result.items,
result.items.required || [],
)
}

return result
// Groq rejects `required` when it is an empty array
return removeEmptyRequired(result)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/typescript/ai-groq/src/utils/schema-converter.ts | head -100

Repository: TanStack/ai

Length of output: 3859


🏁 Script executed:

# Get file size first
wc -l packages/typescript/ai-groq/src/utils/schema-converter.ts

Repository: TanStack/ai

Length of output: 115


🏁 Script executed:

# Look for the makeStructuredOutputCompatible function
rg -n "makeStructuredOutputCompatible" packages/typescript/ai-groq/src/utils/schema-converter.ts -A 30

Repository: TanStack/ai

Length of output: 1322


🏁 Script executed:

# Check the test file for nested object cases
cat packages/typescript/ai-groq/tests/schema-converter.test.ts

Repository: TanStack/ai

Length of output: 2873


🏁 Script executed:

# Find where openai-base is used and check if we can understand its behavior
find . -name "openai-base" -type d 2>/dev/null | head -5

Repository: TanStack/ai

Length of output: 88


🏁 Script executed:

# Search for any documentation or tests that show how makeStructuredOutputCompatible handles empty objects
rg "makeStructuredOutputCompatible" --type ts --type tsx -B 3 -A 10 | head -150

Repository: TanStack/ai

Length of output: 82


🏁 Script executed:

# Check package.json to see openai-base version
rg '"@tanstack/openai-base"' packages/typescript/ai-groq/package.json

Repository: TanStack/ai

Length of output: 98


🏁 Script executed:

# Look for any comments or documentation about the openai-base transformer
rg "openai-base" packages/typescript/ai-groq/src -B 2 -A 2

Repository: TanStack/ai

Length of output: 916


🏁 Script executed:

# Check if there are integration tests or real Groq schema examples
fd "schema" packages/typescript/ai-groq/tests/ -type f

Repository: TanStack/ai

Length of output: 225


🏁 Script executed:

# Check the openai-base transformer implementation
cat packages/typescript/openai-base/src/utils/structured-output.ts

Repository: TanStack/ai

Length of output: 149


🏁 Script executed:

# Alternative: search for the transformer function
rg -n "makeStructuredOutputCompatible" packages/typescript/openai-base/src -B 5 -A 30

Repository: TanStack/ai

Length of output: 50367


🏁 Script executed:

# Look at the openai-base file structure
ls -la packages/typescript/openai-base/src/utils/

Repository: TanStack/ai

Length of output: 293


🏁 Script executed:

# Extract just the key part of makeStructuredOutputCompatible
cat packages/typescript/openai-base/src/utils/schema-converter.ts | sed -n '12,68p'

Repository: TanStack/ai

Length of output: 2166


🏁 Script executed:

# Check if there are any test cases showing nested empty object behavior
rg "type.*object.*properties" packages/typescript/openai-base -A 3 -B 1

Repository: TanStack/ai

Length of output: 1084


Normalize empty object schemas recursively before delegating to openai-base transformer.

The root-level normalization at line 85 only patches { type: 'object' } without properties at the top level. Nested branches like { type: 'object' } inside properties, items, anyOf, or additionalProperties skip normalization because makeStructuredOutputCompatible (in openai-base) only processes objects with both type === 'object' AND properties present. A schema like { type: 'object', properties: { child: { type: 'object' } }, required: ['child'] } passes through unchanged, leaving child: { type: 'object' } without properties: {}, which can fail Groq validation. Add a recursive normalization step before calling makeStructuredOutputCompatible, and include a regression test for nested empty-object branches in the test file.

💡 Fix sketch
+function normalizeObjectSchemas(schema: Record<string, any>): Record<string, any> {
+  const result =
+    schema.type === 'object' && !schema.properties
+      ? { ...schema, properties: {} }
+      : { ...schema }
+
+  if (result.properties && typeof result.properties === 'object') {
+    result.properties = Object.fromEntries(
+      Object.entries(result.properties as Record<string, any>).map(
+        ([key, value]) => [
+          key,
+          typeof value === 'object' && value !== null && !Array.isArray(value)
+            ? normalizeObjectSchemas(value)
+            : value,
+        ],
+      ),
+    )
+  }
+
+  if (result.items && typeof result.items === 'object' && !Array.isArray(result.items)) {
+    result.items = normalizeObjectSchemas(result.items)
+  }
+
+  for (const keyword of ['anyOf', 'oneOf', 'allOf'] as const) {
+    if (Array.isArray(result[keyword])) {
+      result[keyword] = result[keyword].map((entry: Record<string, any>) =>
+        normalizeObjectSchemas(entry),
+      )
+    }
+  }
+
+  if (
+    result.additionalProperties &&
+    typeof result.additionalProperties === 'object' &&
+    !Array.isArray(result.additionalProperties)
+  ) {
+    result.additionalProperties = normalizeObjectSchemas(
+      result.additionalProperties,
+    )
+  }
+
+  return result
+}
+
-  const normalised =
-    schema.type === 'object' && !schema.properties
-      ? { ...schema, properties: {} }
-      : schema
+  const normalised = normalizeObjectSchemas(schema)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-groq/src/utils/schema-converter.ts` around lines 82 -
93, The current normalization only patches a top-level `{ type: 'object' }`
(variable normalised) and misses nested empty-object branches, causing child
schemas without properties to slip through makeStructuredOutputCompatible;
update schema-converter.ts to recursively walk the schema before calling
makeStructuredOutputCompatible and ensure any node with `type === 'object'` but
no `properties` gets `properties: {}` (handle nested locations: `properties`
values, `items`, `anyOf`/`oneOf`/`allOf`, and `additionalProperties`), keep the
existing call to removeEmptyRequired for final cleanup, and add a regression
test that constructs a schema like `{ type: 'object', properties: { child: {
type: 'object' } }, required: ['child'] }` to verify the nested child receives
an empty properties object and passes Groq validation.

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.

1 participant