Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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.
4 changes: 3 additions & 1 deletion packages/typescript/ai-anthropic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@
"test:types": "tsc"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.71.2"
"@anthropic-ai/sdk": "^0.71.2",
"@tanstack/ai-utils": "workspace:*"
},
"peerDependencies": {
"@tanstack/ai": "workspace:^",
"zod": "^4.0.0"
},
"devDependencies": {
"@tanstack/ai": "workspace:*",
"@tanstack/ai-utils": "workspace:*",
"@vitest/coverage-v8": "4.0.14",
"zod": "^4.2.0"
}
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)
}
4 changes: 3 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 All @@ -50,6 +51,7 @@
"devDependencies": {
"@tanstack/ai": "workspace:*",
"@tanstack/ai-client": "workspace:*",
"@tanstack/ai-utils": "workspace:*",
"@vitest/coverage-v8": "4.0.14"
}
}
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
4 changes: 3 additions & 1 deletion packages/typescript/ai-fal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@
"video-generation"
],
"dependencies": {
"@fal-ai/client": "^1.9.4"
"@fal-ai/client": "^1.9.4",
"@tanstack/ai-utils": "workspace:*"
},
"devDependencies": {
"@tanstack/ai": "workspace:*",
"@tanstack/ai-utils": "workspace:*",
"@vitest/coverage-v8": "4.0.14",
"vite": "^7.2.7"
},
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)
}
4 changes: 3 additions & 1 deletion packages/typescript/ai-gemini/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@
"adapter"
],
"dependencies": {
"@google/genai": "^1.43.0"
"@google/genai": "^1.43.0",
"@tanstack/ai-utils": "workspace:*"
},
"peerDependencies": {
"@tanstack/ai": "workspace:^"
},
"devDependencies": {
"@tanstack/ai": "workspace:*",
"@tanstack/ai-utils": "workspace:*",
"@vitest/coverage-v8": "4.0.14",
"vite": "^7.2.7"
}
Expand Down
21 changes: 6 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,16 @@ 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 {
return getApiKeyFromEnv('GEMINI_API_KEY')
}

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)
}
4 changes: 4 additions & 0 deletions packages/typescript/ai-grok/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,13 @@
"adapter"
],
"dependencies": {
"@tanstack/ai-utils": "workspace:*",
"@tanstack/openai-base": "workspace:*",
"openai": "^6.9.1"
},
"devDependencies": {
"@tanstack/ai-utils": "workspace:*",
"@tanstack/openai-base": "workspace:*",
"@vitest/coverage-v8": "4.0.14",
"vite": "^7.2.7"
},
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