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
29 changes: 29 additions & 0 deletions .changeset/tired-spoons-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@tanstack/tests-adapters': minor
'@tanstack/preact-ai-devtools': minor
'@tanstack/react-ai-devtools': minor
'@tanstack/solid-ai-devtools': minor
'@tanstack/smoke-tests-e2e': minor
'@tanstack/ai-openrouter': minor
'@tanstack/ai-anthropic': minor
'@tanstack/ai-devtools-core': minor
'@tanstack/ai-react-ui': minor
'@tanstack/ai-solid-ui': minor
'@tanstack/ai-client': minor
'@tanstack/ai-gemini': minor
'@tanstack/ai-ollama': minor
'@tanstack/ai-openai': minor
'@tanstack/ai-preact': minor
'@tanstack/ai-svelte': minor
'@tanstack/ai-vue-ui': minor
'@tanstack/ai-react': minor
'@tanstack/ai-solid': minor
'@tanstack/ai-grok': minor
'@tanstack/ai-vue': minor
'ts-svelte-chat': minor
'@tanstack/ai': minor
'vanilla-chat': minor
'ts-vue-chat': minor
---

Added embed/embedMany activity and Gemini embeddings adapter
165 changes: 165 additions & 0 deletions packages/typescript/ai-gemini/src/adapters/embedding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { BaseEmbeddingAdapter } from '@tanstack/ai/adapters'
import {
createGeminiClient,
generateId,
getGeminiApiKeyFromEnv,
} from '../utils'
import {
validateTaskType,
validateValue,
} from '../embedding/embedding-provider-options'
import type { GoogleGenAI } from '@google/genai'
import type {
EmbedManyOptions,
EmbedManyResult,
EmbedOptions,
EmbedResult,
} from '@tanstack/ai'
import type { GeminiEmbeddingModels } from '../model-meta'
import type { GeminiClientConfig } from '../utils'
import type {
GeminiEmbeddingModelProviderOptionsByName,
GeminiEmbeddingProviderOptions,
} from '../embedding/embedding-provider-options'

/**
* Configuration for Gemini embedding adapter
*/
export interface GeminiEmbeddingConfig extends GeminiClientConfig {}

export class GeminiEmbeddingAdapter<
TModel extends GeminiEmbeddingModels,
> extends BaseEmbeddingAdapter<TModel, GeminiEmbeddingProviderOptions> {
readonly kind = 'embedding' as const
readonly name = 'gemini' as const

// Type-only property - never assigned at runtime
declare '~types': {
providerOptions: GeminiEmbeddingProviderOptions
modelProviderOptionsByName: GeminiEmbeddingModelProviderOptionsByName
}

private client: GoogleGenAI

constructor(config: GeminiEmbeddingConfig, model: TModel) {
super({}, model)
this.client = createGeminiClient(config)
}

async embed(
options: EmbedOptions<GeminiEmbeddingProviderOptions>,
): Promise<EmbedResult> {
const { model, value, modelOptions } = options

validateValue({ value, model })
validateTaskType({ taskType: modelOptions?.taskType, model })

const { totalTokens } = await this.client.models.countTokens({
model,
contents: value,
})

const { embeddings } = await this.client.models.embedContent({
model,
contents: value,
config: {
...modelOptions,
},
})

return {
embedding: embeddings?.[0]?.values || [],
id: generateId(this.name),
model,
usage: totalTokens ? { totalTokens } : undefined,
}
}

async embedMany(
options: EmbedManyOptions<GeminiEmbeddingProviderOptions>,
): Promise<EmbedManyResult> {
const { model, values, modelOptions } = options

validateValue({ value: values, model })
validateTaskType({ taskType: modelOptions?.taskType, model })

const { totalTokens } = await this.client.models.countTokens({
model,
contents: values,
})

const { embeddings } = await this.client.models.embedContent({
model,
contents: values,
config: {
...modelOptions,
},
})

return {
embeddings: embeddings?.map((embedding) => embedding.values || []) || [],
id: generateId(this.name),
model,
usage: totalTokens ? { totalTokens } : undefined,
}
}
}

/**
* Creates a Gemini embedding adapter with explicit API key.
* Type resolution happens here at the call site.
*
* @param model - The model name (e.g., 'embedding-001')
* @param apiKey - Your Google API key
* @param config - Optional additional configuration
* @returns Configured Gemini embedding adapter instance with resolved types
*
* @example
* ```typescript
* const adapter = createGeminiEmbedding('embedding-001', "your-api-key");
*
* const result = await embed({
* adapter,
* value: 'Hello, world!'
* });
* ```
*/
export function createGeminiEmbedding<TModel extends GeminiEmbeddingModels>(
model: TModel,
apiKey: string,
config?: Omit<GeminiEmbeddingConfig, 'apiKey'>,
): GeminiEmbeddingAdapter<TModel> {
return new GeminiEmbeddingAdapter({ apiKey, ...config }, model)
}

/**
* Creates a Gemini embedding adapter with automatic API key detection from environment variables.
* Type resolution happens here at the call site.
*
* Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in:
* - `process.env` (Node.js)
* - `window.env` (Browser with injected env)
*
* @param model - The model name (e.g., 'embedding-001')
* @param config - Optional configuration (excluding apiKey which is auto-detected)
* @returns Configured Gemini embedding adapter instance with resolved types
* @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found in environment
*
* @example
* ```typescript
* // Automatically uses GOOGLE_API_KEY from environment
* const adapter = geminiEmbedding('embedding-001');
*
* const result = await embed({
* adapter,
* value: 'Hello, world!'
* });
* ```
*/
export function geminiEmbedding<TModel extends GeminiEmbeddingModels>(
model: TModel,
config?: Omit<GeminiEmbeddingConfig, 'apiKey'>,
): GeminiEmbeddingAdapter<TModel> {
const apiKey = getGeminiApiKeyFromEnv()
return createGeminiEmbedding(model, apiKey, config)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { HttpOptions } from '@google/genai'
import type { GeminiEmbeddingModels } from '../model-meta'

const VALID_TASK_TYPES = [
'SEMANTIC_SIMILARITY',
'CLASSIFICATION',
'CLUSTERING',
'RETRIEVAL_DOCUMENT',
'RETRIEVAL_QUERY',
'CODE_RETRIEVAL_QUERY',
'QUESTION_ANSWERING',
'FACT_VERIFICATION',
] as const

type TaskType = (typeof VALID_TASK_TYPES)[number]

export interface GeminiEmbeddingProviderOptions {
/** Used to override HTTP request options. */
httpOptions?: HttpOptions
/**
* Type of task for which the embedding will be used.
*/
taskType?: TaskType
/**
* Title for the text. Only applicable when TaskType is `RETRIEVAL_DOCUMENT`.
*/
title?: string
/**
* Reduced dimension for the output embedding. If set,
* excessive values in the output embedding are truncated from the end.
* Supported by newer models since 2024 only. You cannot set this value if
* using the earlier model (`models/embedding-001`).
*/
outputDimensionality?: number
}

export type GeminiEmbeddingModelProviderOptionsByName = {
[K in GeminiEmbeddingModels]: GeminiEmbeddingProviderOptions
}

/**
* Validates the task type
*/
export function validateTaskType(options: {
taskType: TaskType | undefined
model: string
}) {
const { taskType, model } = options
if (!taskType) return

if (!VALID_TASK_TYPES.includes(taskType)) {
throw new Error(`Invalid task type "${taskType}" for model "${model}".`)
}
}

/**
* Validates the value to embed is not empty
*/
export function validateValue(options: {
value: string | Array<string>
model: string
}): void {
const { value, model } = options
if (Array.isArray(value)) {
if (value.length === 0) {
throw new Error(`Value array cannot be empty for model "${model}".`)
}
for (const v of value) {
if (!v || v.trim().length === 0) {
throw new Error(
`Value array cannot contain empty values for model "${model}".`,
)
}
}
} else {
if (!value || value.trim().length === 0) {
throw new Error(`Value cannot be empty for model "${model}".`)
}
}
}
10 changes: 10 additions & 0 deletions packages/typescript/ai-gemini/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,22 @@ export {
type GeminiTTSProviderOptions,
} from './adapters/tts'

// Embedding adapter
export {
GeminiEmbeddingAdapter,
createGeminiEmbedding,
geminiEmbedding,
type GeminiEmbeddingConfig,
} from './adapters/embedding'

// Re-export models from model-meta for convenience
export { GEMINI_MODELS as GeminiTextModels } from './model-meta'
export { GEMINI_EMBEDDING_MODELS as GeminiEmbeddingModels } from './model-meta'
export { GEMINI_IMAGE_MODELS as GeminiImageModels } from './model-meta'
export { GEMINI_TTS_MODELS as GeminiTTSModels } from './model-meta'
export { GEMINI_TTS_VOICES as GeminiTTSVoices } from './model-meta'
export type { GeminiModels as GeminiTextModel } from './model-meta'
export type { GeminiEmbeddingModels as GeminiEmbeddingModel } from './model-meta'
export type { GeminiImageModels as GeminiImageModel } from './model-meta'
export type { GeminiTTSVoice } from './model-meta'

Expand Down
28 changes: 27 additions & 1 deletion packages/typescript/ai-gemini/src/model-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface ModelMeta<TProviderOptions = unknown> {
name: string
supports: {
input: Array<'text' | 'image' | 'audio' | 'video' | 'document'>
output: Array<'text' | 'image' | 'audio' | 'video'>
output: Array<'text' | 'image' | 'audio' | 'video' | 'embedding'>
capabilities?: Array<
| 'audio_generation'
| 'batch_api'
Expand Down Expand Up @@ -678,6 +678,28 @@ const IMAGEN_3 = {
GeminiCommonConfigOptions &
GeminiCachedContentOptions
>

const GEMINI_EMBEDDING_001 = {
name: 'embedding-001',
max_input_tokens: 2048,
supports: {
input: ['text'],
output: ['embedding'],
},
pricing: {
input: {
normal: 0,
},
output: {
normal: 0.15,
},
},
} as const satisfies ModelMeta<
GeminiToolConfigOptions &
GeminiSafetyOptions &
GeminiCommonConfigOptions &
GeminiCachedContentOptions
>
/**
const VEO_3_1_PREVIEW = {
name: 'veo-3.1-generate-preview',
Expand Down Expand Up @@ -833,6 +855,10 @@ export const GEMINI_MODELS = [

export type GeminiModels = (typeof GEMINI_MODELS)[number]

export const GEMINI_EMBEDDING_MODELS = [GEMINI_EMBEDDING_001.name] as const

export type GeminiEmbeddingModels = (typeof GEMINI_EMBEDDING_MODELS)[number]

export type GeminiImageModels = (typeof GEMINI_IMAGE_MODELS)[number]

export const GEMINI_IMAGE_MODELS = [
Expand Down
Loading
Loading