Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 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
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: delegate shared utilities to `@tanstack/ai-utils` and OpenAI-compatible adapter logic to `@tanstack/openai-base`. No breaking changes — all public APIs remain identical.
3 changes: 2 additions & 1 deletion packages/typescript/ai-anthropic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,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 @@ -41,7 +41,8 @@
"test:types": "tsc"
},
"dependencies": {
"@11labs/client": "^0.2.0"
"@11labs/client": "^0.2.0",
"@tanstack/ai-utils": "workspace:*"
},
"peerDependencies": {
"@tanstack/ai": "workspace:^",
Expand Down
21 changes: 2 additions & 19 deletions packages/typescript/ai-elevenlabs/src/realtime/token.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getApiKeyFromEnv } from '@tanstack/ai-utils'
import type { RealtimeToken, RealtimeTokenAdapter } from '@tanstack/ai'
import type { ElevenLabsRealtimeTokenOptions } from './types'

Expand All @@ -7,25 +8,7 @@ const ELEVENLABS_API_URL = 'https://api.elevenlabs.io/v1'
* Get ElevenLabs API key from environment
*/
function getElevenLabsApiKey(): string {
// Check process.env (Node.js)
if (typeof process !== 'undefined' && process.env.ELEVENLABS_API_KEY) {
return process.env.ELEVENLABS_API_KEY
}

// Check window.env (Browser with injected env)
if (
typeof window !== 'undefined' &&
(window as unknown as { env?: { ELEVENLABS_API_KEY?: string } }).env
?.ELEVENLABS_API_KEY
) {
return (window as unknown as { env: { ELEVENLABS_API_KEY: string } }).env
.ELEVENLABS_API_KEY
}

throw new Error(
'ELEVENLABS_API_KEY not found in environment variables. ' +
'Please set ELEVENLABS_API_KEY in your environment.',
)
return getApiKeyFromEnv('ELEVENLABS_API_KEY')
}

/**
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 @@ -41,7 +41,8 @@
"video-generation"
],
"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 @@ -56,5 +27,5 @@ export function configureFalClient(config?: FalClientConfig): void {
}

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-gemini/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,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 @@ -40,6 +40,8 @@
"adapter"
],
"dependencies": {
"@tanstack/ai-utils": "workspace:*",
"@tanstack/openai-base": "workspace:*",
"openai": "^6.9.1"
},
"devDependencies": {
Expand Down
83 changes: 18 additions & 65 deletions packages/typescript/ai-grok/src/adapters/image.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BaseImageAdapter } from '@tanstack/ai/adapters'
import { createGrokClient, generateId, getGrokApiKeyFromEnv } from '../utils'
import { OpenAICompatibleImageAdapter } from '@tanstack/openai-base'
import { getGrokApiKeyFromEnv, toCompatibleConfig } from '../utils/client'
import {
validateImageSize,
validateNumberOfImages,
Expand All @@ -11,12 +11,6 @@ import type {
GrokImageModelSizeByName,
GrokImageProviderOptions,
} from '../image/image-provider-options'
import type {
GeneratedImage,
ImageGenerationOptions,
ImageGenerationResult,
} from '@tanstack/ai'
import type OpenAI_SDK from 'openai'
import type { GrokClientConfig } from '../utils'

/**
Expand All @@ -37,7 +31,7 @@ export interface GrokImageConfig extends GrokClientConfig {}
*/
export class GrokImageAdapter<
TModel extends GrokImageModel,
> extends BaseImageAdapter<
> extends OpenAICompatibleImageAdapter<
TModel,
GrokImageProviderOptions,
GrokImageModelProviderOptionsByName,
Expand All @@ -46,70 +40,29 @@ export class GrokImageAdapter<
readonly kind = 'image' as const
readonly name = 'grok' as const

private client: OpenAI_SDK

constructor(config: GrokImageConfig, model: TModel) {
super({}, model)
this.client = createGrokClient(config)
super(toCompatibleConfig(config), model, 'grok')
}

async generateImages(
options: ImageGenerationOptions<GrokImageProviderOptions>,
): Promise<ImageGenerationResult> {
const { model, prompt, numberOfImages, size } = options

// Validate inputs
validatePrompt({ prompt, model })
validateImageSize(model, size)
validateNumberOfImages(model, numberOfImages)

// Build request based on model type
const request = this.buildRequest(options)

const response = await this.client.images.generate({
...request,
stream: false,
})

return this.transformResponse(model, response)
protected override validatePrompt(options: {
prompt: string
model: string
}): void {
validatePrompt(options)
}

private buildRequest(
options: ImageGenerationOptions<GrokImageProviderOptions>,
): OpenAI_SDK.Images.ImageGenerateParams {
const { model, prompt, numberOfImages, size, modelOptions } = options

return {
model,
prompt,
n: numberOfImages ?? 1,
size: size as OpenAI_SDK.Images.ImageGenerateParams['size'],
...modelOptions,
}
protected override validateImageSize(
model: string,
size: string | undefined,
): void {
validateImageSize(model, size)
}

private transformResponse(
protected override validateNumberOfImages(
model: string,
response: OpenAI_SDK.Images.ImagesResponse,
): ImageGenerationResult {
const images: Array<GeneratedImage> = (response.data ?? []).map((item) => ({
b64Json: item.b64_json,
url: item.url,
revisedPrompt: item.revised_prompt,
}))

return {
id: generateId(this.name),
model,
images,
usage: response.usage
? {
inputTokens: response.usage.input_tokens,
outputTokens: response.usage.output_tokens,
totalTokens: response.usage.total_tokens,
}
: undefined,
}
numberOfImages: number | undefined,
): void {
validateNumberOfImages(model, numberOfImages)
}
}

Expand Down
Loading
Loading