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
7 changes: 7 additions & 0 deletions .changeset/add-litellm-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/ai-litellm': minor
---

feat: add LiteLLM AI gateway adapter

New `@tanstack/ai-litellm` package that provides a tree-shakeable text adapter for the LiteLLM proxy. Extends `OpenAIBaseChatCompletionsTextAdapter` (same pattern as `ai-groq` and `ai-grok`), giving access to 100+ LLM providers through a single adapter.
60 changes: 60 additions & 0 deletions packages/ai-litellm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"name": "@tanstack/ai-litellm",
"version": "0.0.1",
"type": "module",
"description": "LiteLLM AI gateway adapter for TanStack AI. Access 100+ LLM providers through a single proxy.",
"author": "",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/TanStack/ai.git",
"directory": "packages/ai-litellm"
},
"module": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts",
"exports": {
".": {
"types": "./dist/esm/index.d.ts",
"import": "./dist/esm/index.js"
}
},
"files": [
"dist",
"src"
],
"scripts": {
"build": "vite build",
"clean": "premove ./build ./dist",
"lint:fix": "eslint ./src --fix",
"test:build": "publint --strict",
"test:eslint": "eslint ./src",
"test:lib": "vitest run",
"test:lib:dev": "pnpm test:lib --watch",
"test:types": "tsc"
},
"keywords": [
"ai",
"ai-sdk",
"typescript",
"tanstack",
"litellm",
"adapter",
"llm",
"gateway",
"multi-provider",
"proxy"
],
"devDependencies": {
"@vitest/coverage-v8": "4.0.14",
"vite": "^7.3.3"
},
"peerDependencies": {
"@tanstack/ai": "workspace:^",
"zod": "^4.0.0"
},
"dependencies": {
"@tanstack/ai-utils": "workspace:*",
"@tanstack/openai-base": "workspace:*",
"openai": "^6.41.0"
}
}
69 changes: 69 additions & 0 deletions packages/ai-litellm/src/adapters/text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import OpenAI from 'openai'
import { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base'
import { getLiteLLMApiKeyFromEnv, withLiteLLMDefaults } from '../utils/client'
import type { LiteLLMClientConfig } from '../utils'

/**
* Configuration for the LiteLLM text adapter.
*/
export interface LiteLLMTextConfig extends LiteLLMClientConfig {}

/**
* LiteLLM Text (Chat) Adapter
*
* Tree-shakeable adapter for LiteLLM AI gateway. LiteLLM exposes an
* OpenAI-compatible Chat Completions endpoint, so we drive it with the
* OpenAI SDK via a baseURL override (the same pattern as ai-groq and
* ai-grok).
*
* LiteLLM supports 100+ providers (OpenAI, Anthropic, Google, Azure,
* AWS Bedrock, Ollama, Groq, Mistral, and more) through a single proxy.
* The model string determines the provider routing, e.g.
* "anthropic/claude-sonnet-4-6" or "openai/gpt-4o".
*/
export class LiteLLMTextAdapter extends OpenAIBaseChatCompletionsTextAdapter<
string,
Record<string, any>,
readonly [],
Record<string, never>,
readonly []
> {
override readonly kind = 'text' as const
override readonly name = 'litellm' as const

constructor(config: LiteLLMTextConfig, model: string) {
super(model, 'litellm', new OpenAI(withLiteLLMDefaults(config)))
}
}

/**
* Creates a LiteLLM text adapter with explicit API key.
*
* @example
* ```typescript
* const adapter = createLitellmText('anthropic/claude-sonnet-4-6', 'sk-...');
* ```
*/
export function createLitellmText(
model: string,
apiKey: string,
config?: Omit<LiteLLMTextConfig, 'apiKey'>,
): LiteLLMTextAdapter {
return new LiteLLMTextAdapter({ apiKey, ...config }, model)
}

/**
* Creates a LiteLLM text adapter with API key from `LITELLM_API_KEY`.
*
* @example
* ```typescript
* const adapter = litellmText('anthropic/claude-sonnet-4-6');
* ```
*/
export function litellmText(
model: string,
config?: Omit<LiteLLMTextConfig, 'apiKey'>,
): LiteLLMTextAdapter {
const apiKey = getLiteLLMApiKeyFromEnv()
return createLitellmText(model, apiKey, config)
}
22 changes: 22 additions & 0 deletions packages/ai-litellm/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @module @tanstack/ai-litellm
*
* LiteLLM AI gateway adapter for TanStack AI.
* Provides a tree-shakeable adapter for LiteLLM's OpenAI-compatible proxy,
* giving access to 100+ LLM providers through a single interface.
*/

// Text (Chat) adapter
export {
LiteLLMTextAdapter,
createLitellmText,
litellmText,
type LiteLLMTextConfig,
} from './adapters/text'

// Utilities
export {
getLiteLLMApiKeyFromEnv,
withLiteLLMDefaults,
type LiteLLMClientConfig,
} from './utils'
35 changes: 35 additions & 0 deletions packages/ai-litellm/src/utils/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { getApiKeyFromEnv } from '@tanstack/ai-utils'
import type { ClientOptions } from 'openai'

export interface LiteLLMClientConfig extends Omit<ClientOptions, 'apiKey'> {
apiKey?: string
}

const DEFAULT_LITELLM_BASE_URL = 'http://localhost:4000/v1'

/**
* Gets LiteLLM API key from environment variables.
* @throws Error if LITELLM_API_KEY is not found
*/
export function getLiteLLMApiKeyFromEnv(): string {
try {
return getApiKeyFromEnv('LITELLM_API_KEY')
} catch {
throw new Error(
'LITELLM_API_KEY is required. Please set it in your environment variables or use createLitellmText() with an explicit API key.',
)
}
}

/**
* Returns an OpenAI client config pointing at the LiteLLM proxy.
* Defaults to http://localhost:4000/v1 when no baseURL is provided.
*/
export function withLiteLLMDefaults(
config: LiteLLMClientConfig,
): LiteLLMClientConfig {
return {
...config,
baseURL: config.baseURL || DEFAULT_LITELLM_BASE_URL,
}
}
5 changes: 5 additions & 0 deletions packages/ai-litellm/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export {
getLiteLLMApiKeyFromEnv,
withLiteLLMDefaults,
type LiteLLMClientConfig,
} from './client'
8 changes: 8 additions & 0 deletions packages/ai-litellm/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src", "tests"],
"exclude": ["node_modules", "dist"]
}
36 changes: 36 additions & 0 deletions packages/ai-litellm/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import { tanstackViteConfig } from '@tanstack/vite-config'
import packageJson from './package.json'

const config = defineConfig({
test: {
name: packageJson.name,
dir: './',
watch: false,
globals: true,
environment: 'node',
include: ['tests/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
exclude: [
'node_modules/',
'dist/',
'tests/',
'**/*.test.ts',
'**/*.config.ts',
'**/types.ts',
],
include: ['src/**/*.ts'],
},
},
})

export default mergeConfig(
config,
tanstackViteConfig({
entry: ['./src/index.ts'],
srcDir: './src',
cjs: false,
}),
)
11 changes: 11 additions & 0 deletions testing/e2e/src/lib/feature-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const matrix: Record<Feature, Set<Provider>> = {
'grok',
'openrouter',
'openai-compatible',
'litellm',
]),
'one-shot-text': new Set([
'openai',
Expand All @@ -27,6 +28,7 @@ export const matrix: Record<Feature, Set<Provider>> = {
'grok',
'openrouter',
'openai-compatible',
'litellm',
]),
reasoning: new Set(['openai', 'anthropic', 'gemini']),
'multi-turn': new Set([
Expand All @@ -38,6 +40,7 @@ export const matrix: Record<Feature, Set<Provider>> = {
'grok',
'openrouter',
'openai-compatible',
'litellm',
]),
'tool-calling': new Set([
'openai',
Expand All @@ -48,6 +51,7 @@ export const matrix: Record<Feature, Set<Provider>> = {
'grok',
'openrouter',
'openai-compatible',
'litellm',
]),
'parallel-tool-calls': new Set([
'openai',
Expand All @@ -57,6 +61,7 @@ export const matrix: Record<Feature, Set<Provider>> = {
'grok',
'openrouter',
'openai-compatible',
'litellm',
]),
// Gemini excluded: approval flow timing issues with Gemini's streaming format
'tool-approval': new Set([
Expand All @@ -67,6 +72,7 @@ export const matrix: Record<Feature, Set<Provider>> = {
'grok',
'openrouter',
'openai-compatible',
'litellm',
]),
// Ollama excluded: aimock doesn't support content+toolCalls for /api/chat format
'text-tool-text': new Set([
Expand All @@ -77,6 +83,7 @@ export const matrix: Record<Feature, Set<Provider>> = {
'grok',
'openrouter',
'openai-compatible',
'litellm',
]),
'structured-output': new Set([
'openai',
Expand All @@ -87,6 +94,7 @@ export const matrix: Record<Feature, Set<Provider>> = {
'grok',
'openrouter',
'openai-compatible',
'litellm',
]),
// Streaming structured output: only providers with native streaming JSON
// schema support are listed here. Other providers fall back to the
Expand All @@ -98,6 +106,7 @@ export const matrix: Record<Feature, Set<Provider>> = {
'grok',
'openrouter',
'openai-compatible',
'litellm',
]),
// Multi-turn structured output: every turn produces its own typed
// `structured-output` part on the assistant message, and historical
Expand Down Expand Up @@ -125,6 +134,7 @@ export const matrix: Record<Feature, Set<Provider>> = {
'grok',
'openrouter',
'openai-compatible',
'litellm',
]),
'agentic-structured': new Set([
'openai',
Expand All @@ -135,6 +145,7 @@ export const matrix: Record<Feature, Set<Provider>> = {
'grok',
'openrouter',
'openai-compatible',
'litellm',
]),
// Native-combined-mode adapters only. Each provider's default test model
// (or per-feature override in `features.ts`) must opt into combined mode
Expand Down
2 changes: 2 additions & 0 deletions testing/e2e/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type Provider =
| 'openrouter'
| 'openrouter-responses'
| 'openai-compatible'
| 'litellm'
| 'elevenlabs'

export type Feature =
Expand Down Expand Up @@ -48,6 +49,7 @@ export const ALL_PROVIDERS: Provider[] = [
'openrouter',
'openrouter-responses',
'openai-compatible',
'litellm',
'elevenlabs',
]

Expand Down
1 change: 1 addition & 0 deletions testing/e2e/tests/test-matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const providers: Provider[] = [
'openrouter',
'openrouter-responses',
'openai-compatible',
'litellm',
'elevenlabs',
]

Expand Down