Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0b5b485
feat(ai-utils): add @tanstack/ai-utils package with shared utilities
AlemTuzlak Mar 30, 2026
40bd0cf
fix(ai-utils): align with canonical adapter patterns
AlemTuzlak Mar 30, 2026
8b79f35
feat(openai-base): add @tanstack/openai-base with schema converter, t…
AlemTuzlak Mar 30, 2026
8c9118a
feat(openai-base): add Chat Completions text adapter base class
AlemTuzlak Mar 30, 2026
1d2450d
feat(openai-base): add Responses API text adapter base class
AlemTuzlak Mar 30, 2026
11f4fe2
feat(openai-base): add image, summarize, transcription, TTS, and vide…
AlemTuzlak Mar 30, 2026
ae94262
refactor(ai-openai): delegate to @tanstack/openai-base and @tanstack/…
AlemTuzlak Mar 30, 2026
6dce76a
refactor(ai-grok): delegate to @tanstack/openai-base and @tanstack/ai…
AlemTuzlak Mar 30, 2026
ca4234d
refactor: migrate ai-groq, ai-openrouter, ai-ollama to shared utilities
AlemTuzlak Mar 30, 2026
b8066a7
style: format files with prettier
AlemTuzlak Mar 30, 2026
3d0b191
refactor: migrate ai-anthropic, ai-gemini, ai-fal, ai-elevenlabs to @…
AlemTuzlak Mar 30, 2026
23252bb
chore: add changesets for openai-base extraction
AlemTuzlak Mar 30, 2026
cd6b57a
fix: address CodeRabbit review comments on openai-base extraction
AlemTuzlak Mar 30, 2026
4acda53
fix: resolve eslint and knip failures from full test suite
AlemTuzlak Mar 30, 2026
e3b8f5c
ci: apply automated fixes
autofix-ci[bot] Mar 30, 2026
857a88e
fix: address CodeRabbit review comments
AlemTuzlak Apr 2, 2026
5e425bd
fix: address code review findings
AlemTuzlak Apr 2, 2026
4f0f433
fix: remove unnecessary type assertions in transcription adapter
AlemTuzlak Apr 2, 2026
0bc9886
ci: apply automated fixes
autofix-ci[bot] Apr 2, 2026
a5e3bc4
Merge remote-tracking branch 'origin/main' into feat/extract-openai-b…
AlemTuzlak Apr 13, 2026
8d5121e
Merge branch 'origin/main' into feat/extract-openai-base-and-ai-utils
AlemTuzlak Apr 24, 2026
3965b88
fix(openai-base): align rebased openai-base with current main
AlemTuzlak Apr 24, 2026
51cef2d
Merge branch 'main' into feat/extract-openai-base-and-ai-utils
AlemTuzlak May 6, 2026
ff0c683
fix: address unresolved CodeRabbit findings + audit hygiene regression
AlemTuzlak May 6, 2026
338a54a
fix: address Round 1 cr-loop findings (subject-scope bucket-a)
AlemTuzlak May 6, 2026
47f357a
fix: address Round 2 cr-loop findings
AlemTuzlak May 6, 2026
d6af8ee
fix: address Round 3 cr-loop findings (lifecycle + edge cases)
AlemTuzlak May 6, 2026
da20ffb
chore(examples/ts-react-chat): refresh model picker
AlemTuzlak May 6, 2026
d0b318d
Merge branch 'main' into feat/extract-openai-base-and-ai-utils
AlemTuzlak May 8, 2026
3bed78b
refactor(openai-base, ai-utils, ai-openai, ai-grok): address PR #409 …
AlemTuzlak May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-ai-utils-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-utils': minor
---

New package: shared provider-agnostic utilities for TanStack AI adapters. Includes `generateId`, `getApiKeyFromEnv`, `transformNullsToUndefined`, and `ModelMeta` types with `defineModelMeta` validation helper. Zero runtime dependencies.
5 changes: 5 additions & 0 deletions .changeset/add-openai-base-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/openai-base': minor
---

New package: shared base adapters and utilities for OpenAI-compatible providers. Includes Chat Completions and Responses API text adapter base classes, image/summarize/transcription/TTS/video adapter base classes, schema converter, 15 tool converters, and shared types. Providers extend these base classes to reduce duplication and ensure consistent behavior.
13 changes: 13 additions & 0 deletions .changeset/refactor-providers-to-shared-packages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@tanstack/ai-openai': patch
'@tanstack/ai-grok': patch
'@tanstack/ai-groq': patch
'@tanstack/ai-openrouter': patch
'@tanstack/ai-ollama': patch
'@tanstack/ai-anthropic': patch
'@tanstack/ai-gemini': patch
'@tanstack/ai-fal': patch
'@tanstack/ai-elevenlabs': patch
---

Internal refactor: every provider now delegates `getApiKeyFromEnv` / `generateId` / `transformNullsToUndefined` / `ModelMeta` helpers to the new `@tanstack/ai-utils` package. `ai-openai` and `ai-grok` additionally inherit OpenAI-compatible adapter base classes (Chat Completions / Responses text, image, summarize, transcription, TTS, video) from the new `@tanstack/openai-base` package; `ai-groq` keeps its own `BaseTextAdapter`-derived text adapter (Groq uses the `groq-sdk`, not the OpenAI SDK) but consumes `@tanstack/openai-base`'s schema converter and tool converters. The remaining providers (`ai-anthropic`, `ai-gemini`, `ai-ollama`, `ai-openrouter`, `ai-fal`, `ai-elevenlabs`) only consume `@tanstack/ai-utils` because they speak provider-native protocols, not OpenAI-compatible ones. No breaking changes — all public APIs remain identical.
87 changes: 69 additions & 18 deletions examples/ts-react-chat/src/lib/model-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,48 @@ export interface ModelOption {

export const MODEL_OPTIONS: Array<ModelOption> = [
// OpenAI
{ provider: 'openai', model: 'gpt-5.2', label: 'OpenAI - GPT-5.2' },
{ provider: 'openai', model: 'gpt-5.2-pro', label: 'OpenAI - GPT-5.2 Pro' },
{ provider: 'openai', model: 'gpt-5.1', label: 'OpenAI - GPT-5.1' },
{ provider: 'openai', model: 'gpt-5', label: 'OpenAI - GPT-5' },
{ provider: 'openai', model: 'gpt-5-mini', label: 'OpenAI - GPT-5 Mini' },
{ provider: 'openai', model: 'gpt-5-nano', label: 'OpenAI - GPT-5 Nano' },
{ provider: 'openai', model: 'gpt-4.1', label: 'OpenAI - GPT-4.1' },
{ provider: 'openai', model: 'gpt-4o', label: 'OpenAI - GPT-4o' },
{ provider: 'openai', model: 'gpt-4o-mini', label: 'OpenAI - GPT-4o Mini' },
{ provider: 'openai', model: 'gpt-5', label: 'OpenAI - GPT-5' },

// Anthropic
{
provider: 'anthropic',
model: 'claude-sonnet-4-6',
label: 'Anthropic - Claude Sonnet 4.6',
model: 'claude-opus-4-7',
label: 'Anthropic - Claude Opus 4.7',
},
{
provider: 'anthropic',
model: 'claude-sonnet-4-5-20250929',
label: 'Anthropic - Claude Sonnet 4.5',
model: 'claude-opus-4-6',
label: 'Anthropic - Claude Opus 4.6',
},
{
provider: 'anthropic',
model: 'claude-opus-4-5-20251101',
label: 'Anthropic - Claude Opus 4.5',
model: 'claude-sonnet-4-6',
label: 'Anthropic - Claude Sonnet 4.6',
},
{
provider: 'anthropic',
model: 'claude-sonnet-4-5',
label: 'Anthropic - Claude Sonnet 4.5',
},
{
provider: 'anthropic',
model: 'claude-haiku-4-0-20250514',
label: 'Anthropic - Claude Haiku 4.0',
model: 'claude-haiku-4-5',
label: 'Anthropic - Claude Haiku 4.5',
},

// Gemini
{
provider: 'gemini',
model: 'gemini-2.0-flash',
label: 'Gemini - 2.0 Flash',
model: 'gemini-2.5-pro',
label: 'Gemini - 2.5 Pro',
},
{
provider: 'gemini',
Expand All @@ -54,20 +65,60 @@ export const MODEL_OPTIONS: Array<ModelOption> = [
},
{
provider: 'gemini',
model: 'gemini-2.5-pro',
label: 'Gemini - 2.5 Pro',
model: 'gemini-2.0-flash',
label: 'Gemini - 2.0 Flash',
},

// Openrouter
// Openrouter — multi-provider via OpenRouter's unified API
{
provider: 'openrouter',
model: 'openai/gpt-5.2',
label: 'OpenRouter - OpenAI GPT-5.2',
},
{
provider: 'openrouter',
model: 'openai/gpt-5.1',
label: 'OpenRouter - OpenAI GPT-5.1',
},
{
provider: 'openrouter',
model: 'openai/gpt-5',
label: 'OpenRouter - OpenAI GPT-5',
},
{
provider: 'openrouter',
model: 'openai/gpt-4o',
label: 'OpenRouter - OpenAI GPT-4o',
},
{
provider: 'openrouter',
model: 'anthropic/claude-opus-4.7',
label: 'OpenRouter - Anthropic Claude Opus 4.7',
},
{
provider: 'openrouter',
model: 'anthropic/claude-sonnet-4.6',
label: 'OpenRouter - Anthropic Claude Sonnet 4.6',
},
{
provider: 'openrouter',
model: 'anthropic/claude-haiku-4.5',
label: 'OpenRouter - Anthropic Claude Haiku 4.5',
},
{
provider: 'openrouter',
model: 'google/gemini-2.5-pro',
label: 'OpenRouter - Google Gemini 2.5 Pro',
},
{
provider: 'openrouter',
model: 'openai/chatgpt-4o-latest',
label: 'Openrouter - ChatGPT 4o Latest',
model: 'x-ai/grok-4',
label: 'OpenRouter - xAI Grok 4',
},
{
provider: 'openrouter',
model: 'openai/chatgpt-4o-mini',
label: 'Openrouter - ChatGPT 4o Mini',
model: 'meta-llama/llama-3.3-70b-instruct',
label: 'OpenRouter - Meta Llama 3.3 70B (Groq-routed)',
},

// Ollama
Expand Down
3 changes: 2 additions & 1 deletion packages/typescript/ai-anthropic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"test:types": "tsc"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.71.2"
"@anthropic-ai/sdk": "^0.71.2",
"@tanstack/ai-utils": "workspace:*"
},
"peerDependencies": {
"@tanstack/ai": "workspace:^",
Expand Down
19 changes: 3 additions & 16 deletions packages/typescript/ai-anthropic/src/utils/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Anthropic_SDK from '@anthropic-ai/sdk'
import { generateId as _generateId, getApiKeyFromEnv } from '@tanstack/ai-utils'
import type { ClientOptions } from '@anthropic-ai/sdk'

export interface AnthropicClientConfig extends ClientOptions {
Expand All @@ -22,26 +23,12 @@ export function createAnthropicClient(
* @throws Error if ANTHROPIC_API_KEY is not found
*/
export function getAnthropicApiKeyFromEnv(): string {
const env =
typeof globalThis !== 'undefined' && (globalThis as any).window?.env
? (globalThis as any).window.env
: typeof process !== 'undefined'
? process.env
: undefined
const key = env?.ANTHROPIC_API_KEY

if (!key) {
throw new Error(
'ANTHROPIC_API_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key.',
)
}

return key
return getApiKeyFromEnv('ANTHROPIC_API_KEY')
}

/**
* Generates a unique ID with a prefix
*/
export function generateId(prefix: string): string {
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`
return _generateId(prefix)
}
3 changes: 2 additions & 1 deletion packages/typescript/ai-elevenlabs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
},
"dependencies": {
"@elevenlabs/client": "^1.3.1",
"@elevenlabs/elevenlabs-js": "^2.44.0"
"@elevenlabs/elevenlabs-js": "^2.44.0",
"@tanstack/ai-utils": "workspace:*"
},
"peerDependencies": {
"@tanstack/ai": "workspace:^",
Expand Down
19 changes: 8 additions & 11 deletions packages/typescript/ai-elevenlabs/src/utils/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ElevenLabsClient } from '@elevenlabs/elevenlabs-js'
import {
getApiKeyFromEnv,
generateId as sharedGenerateId,
} from '@tanstack/ai-utils'
import type { ElevenLabsOutputFormat } from '../model-meta'

/**
Expand Down Expand Up @@ -39,14 +43,7 @@ function getEnvironment(): EnvObject | undefined {
}

export function getElevenLabsApiKeyFromEnv(): string {
const key = getEnvironment()?.ELEVENLABS_API_KEY
if (!key) {
throw new Error(
'ELEVENLABS_API_KEY is required. Please set it in your environment ' +
'variables or pass it explicitly to the adapter factory.',
)
}
return key
return getApiKeyFromEnv('ELEVENLABS_API_KEY')
}

export function getElevenLabsAgentIdFromEnv(): string {
Expand Down Expand Up @@ -80,9 +77,9 @@ export function createElevenLabsClient(
})
}

export function generateId(prefix: string): string {
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2)}`
}
// Re-exported from `@tanstack/ai-utils` so existing callers keep working
// while the implementation stays deduped across provider packages.
export const generateId = sharedGenerateId

/**
* Convert an ArrayBuffer to base64 in a cross-runtime way.
Expand Down
3 changes: 2 additions & 1 deletion packages/typescript/ai-fal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"transcription"
],
"dependencies": {
"@fal-ai/client": "^1.9.4"
"@fal-ai/client": "^1.9.4",
"@tanstack/ai-utils": "workspace:*"
},
"devDependencies": {
"@tanstack/ai": "workspace:*",
Expand Down
35 changes: 3 additions & 32 deletions packages/typescript/ai-fal/src/utils/client.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,13 @@
import { fal } from '@fal-ai/client'
import { generateId as _generateId, getApiKeyFromEnv } from '@tanstack/ai-utils'

export interface FalClientConfig {
apiKey: string
proxyUrl?: string
}

interface EnvObject {
FAL_KEY?: string
}

interface WindowWithEnv {
env?: EnvObject
}

function getEnvironment(): EnvObject | undefined {
if (typeof globalThis !== 'undefined') {
const win = (globalThis as { window?: WindowWithEnv }).window
if (win?.env) {
return win.env
}
}
if (typeof process !== 'undefined') {
return process.env as EnvObject
}
return undefined
}

export function getFalApiKeyFromEnv(): string {
const env = getEnvironment()
const key = env?.FAL_KEY

if (!key) {
throw new Error(
'FAL_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key.',
)
}

return key
return getApiKeyFromEnv('FAL_KEY')
}

export function configureFalClient(config?: FalClientConfig): void {
Expand All @@ -48,7 +19,7 @@ export function configureFalClient(config?: FalClientConfig): void {
}

export function generateId(prefix: string): string {
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2)}`
return _generateId(prefix)
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/typescript/ai-gemini/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"adapter"
],
"dependencies": {
"@google/genai": "^1.43.0"
"@google/genai": "^1.43.0",
"@tanstack/ai-utils": "workspace:*"
},
"peerDependencies": {
"@tanstack/ai": "workspace:^"
Expand Down
27 changes: 12 additions & 15 deletions packages/typescript/ai-gemini/src/utils/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { GoogleGenAI } from '@google/genai'
import { generateId as _generateId, getApiKeyFromEnv } from '@tanstack/ai-utils'
import type { GoogleGenAIOptions } from '@google/genai'

export interface GeminiClientConfig extends GoogleGenAIOptions {
Expand All @@ -20,26 +21,22 @@ export function createGeminiClient(config: GeminiClientConfig): GoogleGenAI {
* @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found
*/
export function getGeminiApiKeyFromEnv(): string {
const env =
typeof globalThis !== 'undefined' && (globalThis as any).window?.env
? (globalThis as any).window.env
: typeof process !== 'undefined'
? process.env
: undefined
const key = env?.GOOGLE_API_KEY || env?.GEMINI_API_KEY

if (!key) {
throw new Error(
'GOOGLE_API_KEY or GEMINI_API_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key.',
)
try {
return getApiKeyFromEnv('GOOGLE_API_KEY')
} catch {
try {
return getApiKeyFromEnv('GEMINI_API_KEY')
} catch {
throw new Error(
'GOOGLE_API_KEY or GEMINI_API_KEY is not set. Please set one of these environment variables or pass the API key directly.',
)
}
}

return key
}

/**
* Generates a unique ID with a prefix
*/
export function generateId(prefix: string): string {
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`
return _generateId(prefix)
}
2 changes: 2 additions & 0 deletions packages/typescript/ai-grok/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
"adapter"
],
"dependencies": {
"@tanstack/ai-utils": "workspace:*",
"@tanstack/openai-base": "workspace:*",
"openai": "^6.9.1"
},
"devDependencies": {
Expand Down
Loading
Loading