UNPKG

ai-sdk-provider-gemini-cli

Version:

Community AI SDK provider for Google Gemini using the official CLI/SDK

1 lines 81.1 kB
{"version":3,"sources":["../src/gemini-provider.ts","../src/gemini-language-model.ts","../src/client.ts","../src/message-mapper.ts","../src/tool-mapper.ts","../src/error.ts","../src/logger.ts","../src/validation.ts"],"sourcesContent":["import type {\n ProviderV3,\n LanguageModelV3,\n EmbeddingModelV3,\n ImageModelV3,\n} from '@ai-sdk/provider';\nimport { NoSuchModelError } from '@ai-sdk/provider';\nimport { GeminiLanguageModel } from './gemini-language-model';\nimport type { GeminiProviderOptions } from './types';\nimport { validateAuthOptions } from './validation';\n\nexport interface GeminiProvider extends ProviderV3 {\n (modelId: string, settings?: Record<string, unknown>): LanguageModelV3;\n languageModel(\n modelId: string,\n settings?: Record<string, unknown>\n ): LanguageModelV3;\n chat(modelId: string, settings?: Record<string, unknown>): LanguageModelV3;\n embeddingModel(modelId: string): EmbeddingModelV3;\n imageModel(modelId: string): ImageModelV3;\n}\n\n/**\n * Creates a new Gemini provider instance.\n *\n * @param options - Configuration options for the provider\n * @returns A configured provider function\n * @throws Error if authentication options are invalid\n *\n * @example\n * ```typescript\n * // Using API key authentication\n * const gemini = createGeminiProvider({\n * authType: 'gemini-api-key',\n * apiKey: process.env.GEMINI_API_KEY\n * });\n *\n * // Use with Vercel AI SDK\n * const model = gemini('gemini-1.5-flash');\n * const result = await generateText({\n * model,\n * prompt: 'Hello, world!'\n * });\n * ```\n */\nexport function createGeminiProvider(\n options: GeminiProviderOptions = {}\n): GeminiProvider {\n // Validate authentication options\n const validatedOptions = validateAuthOptions(options);\n\n // Create the language model factory function\n const createLanguageModel = (\n modelId: string,\n settings?: Record<string, unknown>\n ) => {\n return new GeminiLanguageModel({\n modelId,\n providerOptions: validatedOptions,\n settings: {\n maxOutputTokens: 65536, // 64K output tokens for Gemini 2.5 models\n ...settings,\n },\n });\n };\n\n // Create the provider function\n const provider = Object.assign(\n function (modelId: string, settings?: Record<string, unknown>) {\n if (new.target) {\n throw new Error(\n 'The provider function cannot be called with the new keyword.'\n );\n }\n\n return createLanguageModel(modelId, settings);\n },\n {\n specificationVersion: 'v3' as const,\n languageModel: createLanguageModel,\n chat: createLanguageModel,\n embeddingModel: (modelId: string): never => {\n throw new NoSuchModelError({\n modelId,\n modelType: 'embeddingModel',\n message: `Gemini provider does not support embedding models.`,\n });\n },\n imageModel: (modelId: string): never => {\n throw new NoSuchModelError({\n modelId,\n modelType: 'imageModel',\n message: `Gemini provider does not support image models.`,\n });\n },\n }\n ) as GeminiProvider;\n\n return provider;\n}\n","import { randomUUID } from 'node:crypto';\nimport type {\n LanguageModelV3,\n LanguageModelV3CallOptions,\n SharedV3Warning,\n LanguageModelV3FinishReason,\n LanguageModelV3FunctionTool,\n LanguageModelV3StreamPart,\n LanguageModelV3Content,\n LanguageModelV3Usage,\n} from '@ai-sdk/provider';\nimport type {\n ContentGenerator,\n ContentGeneratorConfig,\n} from '@google/gemini-cli-core';\nimport type {\n GenerateContentParameters,\n GenerateContentConfig,\n} from '@google/genai';\n\n/**\n * ThinkingLevel enum for Gemini 3 models.\n * Note: This is defined locally as @google/genai v1.30.0 doesn't export it yet.\n * Values match the official @google/genai v1.34.0 ThinkingLevel enum format.\n * Will be replaced with the official enum when gemini-cli-core upgrades.\n */\nexport enum ThinkingLevel {\n /** Minimizes latency and cost. Best for simple tasks. */\n LOW = 'LOW',\n /** Balanced thinking for most tasks. (Gemini 3 Flash only) */\n MEDIUM = 'MEDIUM',\n /** Maximizes reasoning depth. May take longer for first token. */\n HIGH = 'HIGH',\n /** Matches \"no thinking\" for most queries. (Gemini 3 Flash only) */\n MINIMAL = 'MINIMAL',\n}\nimport { initializeGeminiClient } from './client';\nimport { mapPromptToGeminiFormat } from './message-mapper';\nimport { mapGeminiToolConfig, mapToolsToGeminiFormat } from './tool-mapper';\nimport { mapGeminiError } from './error';\nimport type { GeminiProviderOptions, Logger } from './types';\nimport { getLogger, createVerboseLogger } from './logger';\n\nexport interface GeminiLanguageModelOptions {\n modelId: string;\n providerOptions: GeminiProviderOptions;\n settings?: Record<string, unknown> & {\n logger?: Logger | false;\n verbose?: boolean;\n };\n}\n\n/**\n * Input interface for thinkingConfig settings.\n * Supports both Gemini 3 (thinkingLevel) and Gemini 2.5 (thinkingBudget) models.\n */\nexport interface ThinkingConfigInput {\n /**\n * Thinking level for Gemini 3 models (gemini-3-pro-preview, gemini-3-flash-preview).\n * Accepts case-insensitive strings ('high', 'HIGH', 'High') or ThinkingLevel enum.\n * Valid values: 'low', 'medium', 'high', 'minimal'\n */\n thinkingLevel?: string | ThinkingLevel;\n /**\n * Token budget for thinking in Gemini 2.5 models.\n * Common values: 0 (disabled), 512, 8192 (default), -1 (unlimited)\n */\n thinkingBudget?: number;\n /**\n * Whether to include thinking/reasoning in the response.\n */\n includeThoughts?: boolean;\n}\n\n/**\n * Normalize thinkingLevel string to ThinkingLevel enum (case-insensitive).\n * Returns undefined for invalid values, allowing the API to handle validation.\n */\nfunction normalizeThinkingLevel(level: string): ThinkingLevel | undefined {\n const normalized = level.toUpperCase();\n switch (normalized) {\n case 'LOW':\n return ThinkingLevel.LOW;\n case 'MEDIUM':\n return ThinkingLevel.MEDIUM;\n case 'HIGH':\n return ThinkingLevel.HIGH;\n case 'MINIMAL':\n return ThinkingLevel.MINIMAL;\n default:\n return undefined;\n }\n}\n\n/**\n * Map Gemini finish reasons to Vercel AI SDK finish reasons.\n *\n * @param geminiReason - The finish reason from Gemini API\n * @returns The corresponding AI SDK finish reason with unified and raw values\n *\n * @remarks\n * Mappings:\n * - 'STOP' -> { unified: 'stop', raw: 'STOP' } (normal completion)\n * - 'MAX_TOKENS' -> { unified: 'length', raw: 'MAX_TOKENS' } (hit token limit)\n * - 'SAFETY'/'RECITATION' -> { unified: 'content-filter', raw } (content filtered)\n * - 'OTHER' -> { unified: 'other', raw: 'OTHER' } (other reason)\n * - undefined -> { unified: 'other', raw: undefined } (no reason provided)\n */\nfunction mapGeminiFinishReason(\n geminiReason?: string\n): LanguageModelV3FinishReason {\n switch (geminiReason) {\n case 'STOP':\n return { unified: 'stop', raw: geminiReason };\n case 'MAX_TOKENS':\n return { unified: 'length', raw: geminiReason };\n case 'SAFETY':\n case 'RECITATION':\n return { unified: 'content-filter', raw: geminiReason };\n case 'OTHER':\n return { unified: 'other', raw: geminiReason };\n default:\n return { unified: 'other', raw: geminiReason };\n }\n}\n\n/**\n * Extended ThinkingConfig type that includes thinkingLevel (not yet in @google/genai v1.30.0 types).\n * This is a temporary workaround until the official types are updated.\n * Using Omit to remove any existing thinkingLevel type and replace with our enum.\n */\ntype ExtendedThinkingConfig = Omit<\n NonNullable<GenerateContentConfig['thinkingConfig']>,\n 'thinkingLevel'\n> & {\n thinkingLevel?: ThinkingLevel;\n};\n\n/**\n * Build thinkingConfig from user input, normalizing string thinkingLevel to enum.\n */\nfunction buildThinkingConfig(\n input: ThinkingConfigInput\n): ExtendedThinkingConfig {\n const config = {} as ExtendedThinkingConfig;\n\n // Handle thinkingLevel (string or enum)\n if (input.thinkingLevel !== undefined) {\n if (typeof input.thinkingLevel === 'string') {\n const normalized = normalizeThinkingLevel(input.thinkingLevel);\n if (normalized !== undefined) {\n config.thinkingLevel = normalized;\n }\n // If normalization fails, we skip setting thinkingLevel\n // and let the API handle any validation errors\n } else {\n // Already a ThinkingLevel enum value\n config.thinkingLevel = input.thinkingLevel;\n }\n }\n\n // Handle thinkingBudget (number)\n if (input.thinkingBudget !== undefined) {\n config.thinkingBudget = input.thinkingBudget;\n }\n\n // Handle includeThoughts (boolean)\n if (input.includeThoughts !== undefined) {\n config.includeThoughts = input.includeThoughts;\n }\n\n return config;\n}\n\n/**\n * Prepare generation config with proper handling for JSON mode and thinkingConfig.\n *\n * When JSON response format is requested WITHOUT a schema, we downgrade to\n * text/plain and emit a warning. This aligns with Claude-code provider behavior\n * and prevents raw fenced JSON from leaking to clients.\n *\n * When a schema IS provided, we use native responseJsonSchema for structured output.\n *\n * ThinkingConfig supports both Gemini 3 (thinkingLevel) and Gemini 2.5 (thinkingBudget).\n */\nfunction prepareGenerationConfig(\n options: LanguageModelV3CallOptions,\n settings?: Record<string, unknown>\n): {\n generationConfig: GenerateContentConfig;\n warnings: SharedV3Warning[];\n} {\n const warnings: SharedV3Warning[] = [];\n\n // Extract schema if JSON mode with schema is requested\n const responseFormat = options.responseFormat;\n const isJsonMode = responseFormat?.type === 'json';\n const schema = isJsonMode ? responseFormat.schema : undefined;\n const hasSchema = isJsonMode && schema !== undefined;\n\n // JSON without schema: downgrade to text/plain with warning\n if (isJsonMode && !hasSchema) {\n warnings.push({\n type: 'unsupported',\n feature: 'responseFormat',\n details:\n 'JSON response format without a schema is not supported. Treating as plain text. Provide a schema for structured output.',\n });\n }\n\n // Handle thinkingConfig from options (call-time) and settings (model-level)\n // Merge fields: call-time options override settings per-field (like temperature/topP)\n // Special handling for thinkingLevel: invalid call-time values fall back to settings\n const settingsThinkingConfig = settings?.thinkingConfig as\n | ThinkingConfigInput\n | undefined;\n const optionsThinkingConfig = (options as Record<string, unknown>)\n .thinkingConfig as ThinkingConfigInput | undefined;\n\n // Validate call-time thinkingLevel before merging\n // If invalid, preserve settings thinkingLevel instead of silently dropping it\n let effectiveOptionsThinking = optionsThinkingConfig;\n if (\n optionsThinkingConfig?.thinkingLevel !== undefined &&\n typeof optionsThinkingConfig.thinkingLevel === 'string'\n ) {\n const normalized = normalizeThinkingLevel(\n optionsThinkingConfig.thinkingLevel\n );\n if (normalized === undefined) {\n // Invalid thinkingLevel - remove it so settings value is preserved\n const { thinkingLevel: _, ...rest } = optionsThinkingConfig;\n effectiveOptionsThinking =\n Object.keys(rest).length > 0 ? rest : undefined;\n }\n }\n\n const mergedThinkingConfig =\n settingsThinkingConfig || effectiveOptionsThinking\n ? { ...settingsThinkingConfig, ...effectiveOptionsThinking }\n : undefined;\n\n const thinkingConfig = mergedThinkingConfig\n ? buildThinkingConfig(mergedThinkingConfig)\n : undefined;\n\n const generationConfig: GenerateContentConfig = {\n temperature:\n options.temperature ?? (settings?.temperature as number | undefined),\n topP: options.topP ?? (settings?.topP as number | undefined),\n topK: options.topK ?? (settings?.topK as number | undefined),\n maxOutputTokens:\n options.maxOutputTokens ??\n (settings?.maxOutputTokens as number | undefined),\n stopSequences: options.stopSequences,\n // Only use application/json when we have a schema to enforce it\n responseMimeType: hasSchema ? 'application/json' : 'text/plain',\n // Pass schema directly to Gemini API for native structured output\n responseJsonSchema: hasSchema ? schema : undefined,\n toolConfig: mapGeminiToolConfig(options),\n // Pass thinkingConfig for Gemini 3 (thinkingLevel) or Gemini 2.5 (thinkingBudget)\n // Cast needed because our ThinkingLevel enum isn't recognized by @google/genai v1.30.0 types\n thinkingConfig: thinkingConfig as GenerateContentConfig['thinkingConfig'],\n };\n\n return { generationConfig, warnings };\n}\n\nexport class GeminiLanguageModel implements LanguageModelV3 {\n readonly specificationVersion = 'v3' as const;\n readonly provider = 'gemini-cli-core';\n readonly defaultObjectGenerationMode = 'json' as const;\n readonly supportsImageUrls = false; // CLI Core uses base64 data, not URLs\n readonly supportedUrls = {}; // No native URL support\n readonly supportsStructuredOutputs = true; // Native Gemini responseJsonSchema support\n\n private contentGenerator?: ContentGenerator;\n private config?: ContentGeneratorConfig;\n private initPromise?: Promise<void>;\n\n readonly modelId: string;\n readonly settings?: Record<string, unknown>;\n private providerOptions: GeminiProviderOptions;\n private logger: Logger;\n\n constructor(options: GeminiLanguageModelOptions) {\n this.modelId = options.modelId;\n this.providerOptions = options.providerOptions;\n this.settings = options.settings;\n\n // Create logger that respects verbose setting\n const baseLogger = getLogger(options.settings?.logger);\n this.logger = createVerboseLogger(\n baseLogger,\n options.settings?.verbose ?? false\n );\n }\n\n private async ensureInitialized(): Promise<{\n contentGenerator: ContentGenerator;\n config: ContentGeneratorConfig;\n }> {\n if (this.contentGenerator && this.config) {\n return { contentGenerator: this.contentGenerator, config: this.config };\n }\n\n if (!this.initPromise) {\n this.initPromise = this.initialize();\n }\n\n await this.initPromise;\n return { contentGenerator: this.contentGenerator!, config: this.config! };\n }\n\n private async initialize(): Promise<void> {\n try {\n const { client, config } = await initializeGeminiClient(\n this.providerOptions,\n this.modelId\n );\n this.contentGenerator = client;\n this.config = config;\n } catch (error) {\n throw new Error(`Failed to initialize Gemini model: ${String(error)}`);\n }\n }\n\n /**\n * Non-streaming generation method\n */\n async doGenerate(options: LanguageModelV3CallOptions): Promise<{\n content: LanguageModelV3Content[];\n finishReason: LanguageModelV3FinishReason;\n usage: LanguageModelV3Usage;\n rawCall: {\n rawPrompt: unknown;\n rawSettings: Record<string, unknown>;\n };\n rawResponse?: {\n body?: unknown;\n };\n response?: {\n id?: string;\n timestamp?: Date;\n modelId?: string;\n };\n warnings: SharedV3Warning[];\n }> {\n this.logger.debug(\n `[gemini-cli] Starting doGenerate request with model: ${this.modelId}`\n );\n\n try {\n const { contentGenerator } = await this.ensureInitialized();\n\n // Map the prompt to Gemini format\n const { contents, systemInstruction } = mapPromptToGeminiFormat(options);\n\n this.logger.debug(\n `[gemini-cli] Request mode: ${options.responseFormat?.type === 'json' ? 'object-json' : 'regular'}, response format: ${options.responseFormat?.type ?? 'none'}`\n );\n\n this.logger.debug(\n `[gemini-cli] Converted ${options.prompt.length} messages`\n );\n\n // Prepare generation config with proper JSON mode handling\n // (downgrades to text/plain with warning if JSON requested without schema)\n const { generationConfig, warnings } = prepareGenerationConfig(\n options,\n this.settings\n );\n\n // Map tools if provided in regular mode\n let tools;\n if (options.tools) {\n // Filter to only function tools (not provider-defined tools)\n const functionTools = options.tools.filter(\n (tool): tool is LanguageModelV3FunctionTool =>\n tool.type === 'function'\n );\n if (functionTools.length > 0) {\n tools = mapToolsToGeminiFormat(functionTools);\n }\n }\n\n // Create the request parameters\n const request: GenerateContentParameters = {\n model: this.modelId,\n contents,\n config: {\n ...generationConfig,\n systemInstruction: systemInstruction,\n tools: tools,\n },\n };\n\n // Set up abort handling\n let abortListener: (() => void) | undefined;\n if (options.abortSignal) {\n // Check if already aborted\n if (options.abortSignal.aborted) {\n const abortError = new Error('Request aborted');\n abortError.name = 'AbortError';\n throw abortError;\n }\n\n // Set up listener for abort signal\n // LIMITATION: The gemini-cli-core library doesn't expose request cancellation\n // We can only check abort status before/after the request, not cancel in-flight\n abortListener = () => {\n // Track abort state - actual cancellation happens via status checks\n };\n options.abortSignal.addEventListener('abort', abortListener, {\n once: true,\n });\n }\n\n // Generate content (new signature requires userPromptId)\n let response;\n const startTime = Date.now();\n try {\n this.logger.debug('[gemini-cli] Executing generateContent request');\n\n response = await contentGenerator.generateContent(\n request,\n randomUUID()\n );\n\n const duration = Date.now() - startTime;\n this.logger.info(\n `[gemini-cli] Request completed - Duration: ${duration}ms`\n );\n\n // Check if aborted during generation\n if (options.abortSignal?.aborted) {\n const abortError = new Error('Request aborted');\n abortError.name = 'AbortError';\n throw abortError;\n }\n } finally {\n // Clean up abort listener\n if (options.abortSignal && abortListener) {\n options.abortSignal.removeEventListener('abort', abortListener);\n }\n }\n\n // Extract the result\n const candidate = response.candidates?.[0];\n const responseContent = candidate?.content;\n\n // Build content array for v3 format\n const content: LanguageModelV3Content[] = [];\n let hasToolCalls = false;\n\n if (responseContent?.parts) {\n for (const part of responseContent.parts) {\n if (part.text) {\n // With native responseJsonSchema, the output is already clean JSON\n content.push({\n type: 'text',\n text: part.text,\n });\n } else if (part.functionCall) {\n hasToolCalls = true;\n content.push({\n type: 'tool-call',\n toolCallId: randomUUID(),\n toolName: part.functionCall.name || '',\n input: JSON.stringify(part.functionCall.args || {}),\n } as LanguageModelV3Content);\n }\n }\n }\n\n // Calculate token usage\n const inputTokens = response.usageMetadata?.promptTokenCount || 0;\n const outputTokens = response.usageMetadata?.candidatesTokenCount || 0;\n const totalTokens = inputTokens + outputTokens;\n\n const usage: LanguageModelV3Usage = {\n inputTokens: {\n total: inputTokens,\n noCache: undefined,\n cacheRead: undefined,\n cacheWrite: undefined,\n },\n outputTokens: {\n total: outputTokens,\n text: undefined,\n reasoning: undefined,\n },\n };\n\n this.logger.debug(\n `[gemini-cli] Token usage - Input: ${inputTokens}, Output: ${outputTokens}, Total: ${totalTokens}`\n );\n\n // Determine finish reason - use 'tool-calls' if tools were called\n const finishReason = hasToolCalls\n ? ({\n unified: 'tool-calls',\n raw: candidate?.finishReason,\n } as LanguageModelV3FinishReason)\n : mapGeminiFinishReason(candidate?.finishReason);\n this.logger.debug(`[gemini-cli] Finish reason: ${finishReason.unified}`);\n\n return {\n content,\n finishReason,\n usage,\n rawCall: {\n rawPrompt: { contents, systemInstruction, generationConfig, tools },\n rawSettings: generationConfig as Record<string, unknown>,\n },\n rawResponse: {\n body: response,\n },\n response: {\n id: randomUUID(),\n timestamp: new Date(),\n modelId: this.modelId,\n },\n warnings,\n };\n } catch (error) {\n this.logger.debug(\n `[gemini-cli] Error during doGenerate: ${error instanceof Error ? error.message : String(error)}`\n );\n throw mapGeminiError(error);\n }\n }\n\n /**\n * Streaming generation method\n */\n async doStream(options: LanguageModelV3CallOptions): Promise<{\n stream: ReadableStream<LanguageModelV3StreamPart>;\n rawCall: {\n rawPrompt: unknown;\n rawSettings: Record<string, unknown>;\n };\n }> {\n this.logger.debug(\n `[gemini-cli] Starting doStream request with model: ${this.modelId}`\n );\n\n try {\n const { contentGenerator } = await this.ensureInitialized();\n\n // Map the prompt to Gemini format\n const { contents, systemInstruction } = mapPromptToGeminiFormat(options);\n\n this.logger.debug(\n `[gemini-cli] Stream mode: ${options.responseFormat?.type === 'json' ? 'object-json' : 'regular'}, response format: ${options.responseFormat?.type ?? 'none'}`\n );\n\n this.logger.debug(\n `[gemini-cli] Converted ${options.prompt.length} messages for streaming`\n );\n\n // Prepare generation config with proper JSON mode handling\n // (downgrades to text/plain with warning if JSON requested without schema)\n const { generationConfig, warnings } = prepareGenerationConfig(\n options,\n this.settings\n );\n\n // Map tools if provided in regular mode\n let tools;\n if (options.tools) {\n // Filter to only function tools (not provider-defined tools)\n const functionTools = options.tools.filter(\n (tool): tool is LanguageModelV3FunctionTool =>\n tool.type === 'function'\n );\n if (functionTools.length > 0) {\n tools = mapToolsToGeminiFormat(functionTools);\n }\n }\n\n // Create the request parameters\n const request: GenerateContentParameters = {\n model: this.modelId,\n contents,\n config: {\n ...generationConfig,\n systemInstruction: systemInstruction,\n tools: tools,\n },\n };\n\n // Set up abort handling\n let abortListener: (() => void) | undefined;\n if (options.abortSignal) {\n // Check if already aborted\n if (options.abortSignal.aborted) {\n const abortError = new Error('Request aborted');\n abortError.name = 'AbortError';\n throw abortError;\n }\n\n // Set up listener for abort signal\n // LIMITATION: The gemini-cli-core library doesn't expose stream cancellation\n // We can only check abort status during iteration, not cancel the underlying stream\n abortListener = () => {\n // Track abort state - actual cancellation happens via status checks\n };\n options.abortSignal.addEventListener('abort', abortListener, {\n once: true,\n });\n }\n\n // Create streaming response (new signature requires userPromptId)\n let streamResponse;\n try {\n this.logger.debug(\n '[gemini-cli] Starting generateContentStream request'\n );\n\n streamResponse = await contentGenerator.generateContentStream(\n request,\n randomUUID()\n );\n\n // Check if aborted during stream creation\n if (options.abortSignal?.aborted) {\n const abortError = new Error('Request aborted');\n abortError.name = 'AbortError';\n throw abortError;\n }\n } catch (error) {\n // Clean up abort listener on error\n if (options.abortSignal && abortListener) {\n options.abortSignal.removeEventListener('abort', abortListener);\n }\n throw error;\n }\n\n // Capture modelId, logger, and warnings for use in stream\n const modelId = this.modelId;\n const logger = this.logger;\n const streamWarnings = warnings;\n\n // Transform the stream to AI SDK v6 format\n const stream = new ReadableStream<LanguageModelV3StreamPart>({\n async start(controller) {\n try {\n // Check for abort signal in stream\n if (options.abortSignal?.aborted) {\n const abortError = new Error('Request aborted');\n abortError.name = 'AbortError';\n controller.error(abortError);\n return;\n }\n let totalInputTokens = 0;\n let totalOutputTokens = 0;\n\n // Track text streaming lifecycle - stable id per text block\n let textPartId: string | undefined;\n let hasToolCalls = false;\n\n // Emit stream-start event with any warnings\n controller.enqueue({\n type: 'stream-start',\n warnings: streamWarnings,\n });\n\n const streamStartTime = Date.now();\n logger.debug('[gemini-cli] Stream started, processing chunks');\n\n for await (const chunk of streamResponse) {\n // Check if aborted during streaming\n if (options.abortSignal?.aborted) {\n const abortError = new Error('Request aborted');\n abortError.name = 'AbortError';\n controller.error(abortError);\n return; // Return after error to prevent further processing\n }\n\n const candidate = chunk.candidates?.[0];\n const content = candidate?.content;\n\n // Update token counts if available\n if (chunk.usageMetadata) {\n totalInputTokens = chunk.usageMetadata.promptTokenCount || 0;\n totalOutputTokens =\n chunk.usageMetadata.candidatesTokenCount || 0;\n }\n\n if (content?.parts) {\n for (const part of content.parts) {\n if (part.text) {\n // Emit text-start if this is the first text chunk\n if (!textPartId) {\n textPartId = randomUUID();\n controller.enqueue({\n type: 'text-start',\n id: textPartId,\n });\n }\n\n // Stream text delta with stable id\n controller.enqueue({\n type: 'text-delta',\n id: textPartId,\n delta: part.text,\n });\n } else if (part.functionCall) {\n hasToolCalls = true;\n // Emit tool call as a single event\n controller.enqueue({\n type: 'tool-call',\n toolCallId: randomUUID(),\n toolName: part.functionCall.name || '',\n input: JSON.stringify(part.functionCall.args || {}),\n });\n }\n }\n }\n\n if (candidate?.finishReason) {\n const duration = Date.now() - streamStartTime;\n logger.info(\n `[gemini-cli] Stream completed - Duration: ${duration}ms`\n );\n\n logger.debug(\n `[gemini-cli] Stream token usage - Input: ${totalInputTokens}, Output: ${totalOutputTokens}, Total: ${totalInputTokens + totalOutputTokens}`\n );\n\n // Close text part if it was opened\n if (textPartId) {\n controller.enqueue({\n type: 'text-end',\n id: textPartId,\n });\n }\n\n // Determine finish reason - use 'tool-calls' if tools were called\n const finishReason = hasToolCalls\n ? ({\n unified: 'tool-calls',\n raw: candidate.finishReason,\n } as LanguageModelV3FinishReason)\n : mapGeminiFinishReason(candidate.finishReason);\n logger.debug(\n `[gemini-cli] Stream finish reason: ${finishReason.unified}`\n );\n\n // Emit response metadata\n controller.enqueue({\n type: 'response-metadata',\n id: randomUUID(),\n timestamp: new Date(),\n modelId: modelId,\n });\n\n // Emit finish event\n controller.enqueue({\n type: 'finish',\n finishReason,\n usage: {\n inputTokens: {\n total: totalInputTokens,\n noCache: undefined,\n cacheRead: undefined,\n cacheWrite: undefined,\n },\n outputTokens: {\n total: totalOutputTokens,\n text: undefined,\n reasoning: undefined,\n },\n },\n });\n }\n }\n\n logger.debug('[gemini-cli] Stream finalized, closing stream');\n controller.close();\n } catch (error) {\n logger.debug(\n `[gemini-cli] Error during doStream: ${error instanceof Error ? error.message : String(error)}`\n );\n controller.error(mapGeminiError(error));\n } finally {\n // Clean up abort listener\n if (options.abortSignal && abortListener) {\n options.abortSignal.removeEventListener('abort', abortListener);\n }\n }\n },\n cancel: () => {\n // Clean up abort listener on cancel\n if (options.abortSignal && abortListener) {\n options.abortSignal.removeEventListener('abort', abortListener);\n }\n },\n });\n\n return {\n stream,\n rawCall: {\n rawPrompt: { contents, systemInstruction, generationConfig, tools },\n rawSettings: generationConfig as Record<string, unknown>,\n },\n };\n } catch (error) {\n this.logger.debug(\n `[gemini-cli] Error creating stream: ${error instanceof Error ? error.message : String(error)}`\n );\n throw mapGeminiError(error);\n }\n }\n}\n","import { randomUUID } from 'node:crypto';\nimport type {\n ContentGenerator,\n ContentGeneratorConfig,\n} from '@google/gemini-cli-core';\nimport {\n createContentGenerator,\n createContentGeneratorConfig,\n AuthType,\n} from '@google/gemini-cli-core';\nimport type { GeminiProviderOptions } from './types';\n\nexport interface GeminiClient {\n client: ContentGenerator;\n config: ContentGeneratorConfig;\n sessionId: string;\n}\n\n/**\n * Initializes the Gemini client with the provided authentication options\n */\nexport async function initializeGeminiClient(\n options: GeminiProviderOptions,\n modelId: string\n): Promise<GeminiClient> {\n // Map our auth types to Gemini CLI Core auth types\n let authType: AuthType | undefined;\n\n if (options.authType === 'api-key' || options.authType === 'gemini-api-key') {\n authType = AuthType.USE_GEMINI;\n } else if (options.authType === 'vertex-ai') {\n authType = AuthType.USE_VERTEX_AI;\n } else if (\n options.authType === 'oauth' ||\n options.authType === 'oauth-personal'\n ) {\n authType = AuthType.LOGIN_WITH_GOOGLE;\n } else if (options.authType === 'google-auth-library') {\n // Google Auth Library is not directly supported by AuthType enum\n // We'll need to handle this differently or use a default\n authType = AuthType.USE_GEMINI;\n }\n\n // Generate a stable session ID for this provider instance\n const sessionId = randomUUID();\n\n // Phase 1: Core config methods with safe defaults\n const baseConfig = {\n // Required methods (currently working)\n getModel: () => modelId,\n getProxy: () =>\n options.proxy ||\n process.env.HTTP_PROXY ||\n process.env.HTTPS_PROXY ||\n undefined,\n getUsageStatisticsEnabled: () => false, // Disable telemetry by default\n getContentGeneratorConfig: () => ({\n authType: authType, // Keep as AuthType | undefined for consistency\n model: modelId,\n apiKey: 'apiKey' in options ? options.apiKey : undefined,\n vertexai: options.authType === 'vertex-ai' ? true : undefined,\n proxy: options.proxy,\n }),\n\n // Core safety methods - most likely to be called\n getSessionId: () => sessionId,\n getDebugMode: () => false,\n getTelemetryEnabled: () => false,\n getTargetDir: () => process.cwd(),\n getFullContext: () => false,\n getIdeMode: () => false,\n getCoreTools: () => [],\n getExcludeTools: () => [],\n getMaxSessionTurns: () => 100,\n getFileFilteringRespectGitIgnore: () => true,\n\n // OAuth-specific methods (required for LOGIN_WITH_GOOGLE auth)\n isBrowserLaunchSuppressed: () => false, // Allow browser launch for OAuth flow\n\n // NEW in 0.20.0 - JIT Context & Memory\n getContextManager: () => undefined,\n getGlobalMemory: () => '',\n getEnvironmentMemory: () => '',\n\n // NEW in 0.20.0 - Hook System\n getHookSystem: () => undefined,\n\n // NEW in 0.20.0 - Model Availability Service (replaces getUseModelRouter)\n getModelAvailabilityService: () => undefined,\n\n // NEW in 0.20.0 - Shell Timeout (default: 2 minutes)\n getShellToolInactivityTimeout: () => 120000,\n\n // NEW in 0.20.0 - Experiments (async getter)\n getExperimentsAsync: () => Promise.resolve(undefined),\n };\n\n // Phase 2: Proxy wrapper to catch any unknown method calls\n const configMock = new Proxy(baseConfig, {\n get(target, prop) {\n if (prop in target) {\n return target[prop as keyof typeof target];\n }\n\n // Log unknown method calls (helps identify what else might be needed)\n if (typeof prop === 'string') {\n // Handle different method patterns\n if (\n prop.startsWith('get') ||\n prop.startsWith('is') ||\n prop.startsWith('has')\n ) {\n if (process.env.DEBUG) {\n console.warn(\n `[ai-sdk-provider-gemini-cli] Unknown config method called: ${prop}()`\n );\n }\n\n // Return safe defaults based on method prefix and naming patterns\n return () => {\n // Boolean methods (is*, has*)\n if (prop.startsWith('is') || prop.startsWith('has')) {\n return false; // Safe default for boolean checks\n }\n\n // Getter methods (get*)\n if (prop.startsWith('get')) {\n // Return undefined for most unknown methods (safest default)\n if (prop.includes('Enabled') || prop.includes('Mode')) {\n return false; // Booleans default to false\n }\n if (\n prop.includes('Registry') ||\n prop.includes('Client') ||\n prop.includes('Service') ||\n prop.includes('Manager')\n ) {\n return undefined; // Objects/services default to undefined\n }\n if (prop.includes('Memory')) {\n return ''; // Memory methods return empty string\n }\n if (prop.includes('Timeout')) {\n return 120000; // Timeout methods default to 2 minutes\n }\n if (prop.includes('Config') || prop.includes('Options')) {\n return {}; // Config objects default to empty\n }\n if (prop.includes('Command') || prop.includes('Path')) {\n return undefined; // Strings default to undefined\n }\n return undefined; // Default fallback\n }\n\n return undefined; // Fallback for any other pattern\n };\n }\n }\n\n return undefined;\n },\n });\n\n // Create the configuration\n const config = await createContentGeneratorConfig(\n configMock as unknown as Parameters<typeof createContentGeneratorConfig>[0],\n authType\n );\n\n // Apply additional configuration based on auth type\n if (\n (options.authType === 'api-key' || options.authType === 'gemini-api-key') &&\n options.apiKey\n ) {\n config.apiKey = options.apiKey;\n } else if (options.authType === 'vertex-ai' && options.vertexAI) {\n config.vertexai = true;\n // Note: Vertex AI project/location configuration might need to be\n // handled through environment variables or other means\n }\n\n // Create content generator - pass the configMock as the second parameter and sessionId\n const client = await createContentGenerator(\n config,\n configMock as unknown as Parameters<typeof createContentGenerator>[1],\n sessionId\n );\n\n return { client, config, sessionId };\n}\n","import type {\n LanguageModelV3CallOptions,\n LanguageModelV3FilePart,\n LanguageModelV3Message,\n} from '@ai-sdk/provider';\nimport type { Content, Part } from '@google/genai';\n\nexport interface GeminiPromptResult {\n contents: Content[];\n systemInstruction?: Content;\n}\n\n/**\n * Maps Vercel AI SDK messages to Gemini format\n *\n * Note: Schema is now passed directly via responseJsonSchema in the generation config,\n * so we no longer inject schema instructions into the prompt.\n */\nexport function mapPromptToGeminiFormat(\n options: LanguageModelV3CallOptions\n): GeminiPromptResult {\n const messages = options.prompt;\n const contents: Content[] = [];\n let systemInstruction: Content | undefined;\n\n for (const message of messages) {\n switch (message.role) {\n case 'system':\n // Gemini uses a separate systemInstruction field\n systemInstruction = {\n role: 'user',\n parts: [{ text: message.content }],\n };\n break;\n\n case 'user':\n contents.push(mapUserMessage(message));\n break;\n\n case 'assistant':\n contents.push(mapAssistantMessage(message));\n break;\n\n case 'tool': {\n // Tool results in v6 have typed output union\n const parts: Part[] = [];\n for (const part of message.content) {\n if (part.type === 'tool-result') {\n // Handle new ToolResultOutput union types in v6\n const output = part.output;\n let resultValue: Record<string, unknown>;\n\n if (output.type === 'text' || output.type === 'error-text') {\n resultValue = { result: output.value };\n } else if (output.type === 'json' || output.type === 'error-json') {\n // JSON values can be objects, arrays, strings, numbers, booleans, or null\n // Gemini expects an object, so wrap non-object values\n const jsonValue = output.value;\n if (\n jsonValue !== null &&\n typeof jsonValue === 'object' &&\n !Array.isArray(jsonValue)\n ) {\n resultValue = jsonValue as Record<string, unknown>;\n } else {\n resultValue = { result: jsonValue };\n }\n } else if (output.type === 'execution-denied') {\n resultValue = {\n result: `[Execution denied${output.reason ? `: ${output.reason}` : ''}]`,\n };\n } else if (output.type === 'content') {\n // Handle content array - extract text parts\n const textContent = output.value\n .filter(\n (p): p is { type: 'text'; text: string } => p.type === 'text'\n )\n .map((p) => p.text)\n .join('\\n');\n resultValue = { result: textContent };\n } else {\n resultValue = { result: '[Unknown output type]' };\n }\n\n parts.push({\n functionResponse: {\n name: part.toolName,\n response: resultValue,\n },\n });\n }\n }\n contents.push({\n role: 'user',\n parts,\n });\n break;\n }\n }\n }\n\n return { contents, systemInstruction };\n}\n\n/**\n * Maps a user message to Gemini format\n */\nfunction mapUserMessage(\n message: LanguageModelV3Message & { role: 'user' }\n): Content {\n const parts: Part[] = [];\n\n for (const part of message.content) {\n switch (part.type) {\n case 'text':\n parts.push({ text: part.text });\n break;\n\n case 'file': {\n // Handle file parts (images, PDF, audio, video)\n const mediaType = part.mediaType || 'application/octet-stream';\n if (\n mediaType.startsWith('image/') ||\n mediaType.startsWith('audio/') ||\n mediaType.startsWith('video/') ||\n mediaType === 'application/pdf'\n ) {\n parts.push(mapFilePart(part));\n } else {\n throw new Error(`Unsupported file type: ${mediaType}`);\n }\n break;\n }\n }\n }\n\n return { role: 'user', parts };\n}\n\n/**\n * Maps an assistant message to Gemini format\n */\nfunction mapAssistantMessage(\n message: LanguageModelV3Message & { role: 'assistant' }\n): Content {\n const parts: Part[] = [];\n\n for (const part of message.content) {\n switch (part.type) {\n case 'text':\n parts.push({ text: part.text });\n break;\n\n case 'tool-call':\n // In v5, tool calls have input as an object already\n parts.push({\n functionCall: {\n name: part.toolName,\n args: (part.input || {}) as Record<string, unknown>,\n },\n });\n break;\n }\n }\n\n return { role: 'model', parts };\n}\n\n/**\n * Maps a file part to Gemini format\n */\nfunction mapFilePart(part: LanguageModelV3FilePart): Part {\n if (part.data instanceof URL) {\n throw new Error(\n 'URL files are not supported by Gemini CLI Core. Please provide base64-encoded data.'\n );\n }\n\n // Extract mime type and base64 data\n const mimeType = part.mediaType || 'application/octet-stream';\n let base64Data: string;\n\n if (typeof part.data === 'string') {\n // Already base64 encoded\n base64Data = part.data;\n } else if (part.data instanceof Uint8Array) {\n // Convert Uint8Array to base64\n base64Data = Buffer.from(part.data).toString('base64');\n } else {\n throw new Error('Unsupported file format');\n }\n\n return {\n inlineData: {\n mimeType,\n data: base64Data,\n },\n };\n}\n","import type {\n LanguageModelV3CallOptions,\n LanguageModelV3FunctionTool,\n LanguageModelV3ToolChoice,\n} from '@ai-sdk/provider';\nimport {\n Tool,\n FunctionDeclaration,\n Schema,\n ToolConfig,\n FunctionCallingConfigMode,\n} from '@google/genai';\nimport { z } from 'zod';\n\n// Type for JSON Schema objects with common properties\ninterface JsonSchemaObject {\n $schema?: string;\n $ref?: string;\n $defs?: unknown;\n definitions?: unknown;\n properties?: Record<string, unknown>;\n items?: unknown;\n additionalProperties?: unknown;\n allOf?: unknown[];\n anyOf?: unknown[];\n oneOf?: unknown[];\n [key: string]: unknown;\n}\n\n/**\n * Maps Vercel AI SDK tools to Gemini format\n */\nexport function mapToolsToGeminiFormat(\n tools: LanguageModelV3FunctionTool[]\n): Tool[] {\n const functionDeclarations: FunctionDeclaration[] = [];\n\n for (const tool of tools) {\n functionDeclarations.push({\n name: tool.name,\n description: tool.description,\n parameters: convertToolParameters(tool.inputSchema),\n });\n }\n\n return [{ functionDeclarations }];\n}\n\n/**\n * Attempts to convert a Zod schema to JSON Schema using available methods\n */\nfunction convertZodToJsonSchema(zodSchema: z.ZodSchema): unknown {\n // Try Zod v4's native toJSONSchema function first (if available)\n const zodWithToJSONSchema = z as unknown as {\n toJSONSchema?: (schema: z.ZodSchema) => unknown;\n };\n\n if (\n zodWithToJSONSchema.toJSONSchema &&\n typeof zodWithToJSONSchema.toJSONSchema === 'function'\n ) {\n try {\n // Zod v4 uses z.toJSONSchema(schema) as a standalone function\n return zodWithToJSONSchema.toJSONSchema(zodSchema);\n } catch {\n // Method exists but failed, try fallback\n }\n }\n\n // Try zod-to-json-schema for Zod v3 compatibility\n try {\n // Lazy load zod-to-json-schema to avoid import errors with Zod v4\n // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment\n const zodToJsonSchemaModule = require('zod-to-json-schema');\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call\n return zodToJsonSchemaModule.zodToJsonSchema(zodSchema);\n } catch {\n // zod-to-json-schema not available or not compatible\n }\n\n // No conversion method available\n console.warn(\n 'Unable to convert Zod schema to JSON Schema. ' +\n 'For Zod v3, install zod-to-json-schema. ' +\n 'For Zod v4, use z.toJSONSchema() function.'\n );\n\n // Return a basic object schema as fallback\n return { type: 'object' };\n}\n\n/**\n * Converts tool parameters from Zod schema or JSON schema to Gemini format\n */\nfunction convertToolParameters(parameters: unknown): Schema {\n // If it's already a plain object (JSON schema), clean it\n if (isJsonSchema(parameters)) {\n return cleanJsonSchema(parameters as JsonSchemaObject) as Schema;\n }\n\n // If it's a Zod schema, convert to JSON schema first\n if (isZodSchema(parameters)) {\n const jsonSchema = convertZodToJsonSchema(parameters as z.ZodSchema);\n return cleanJsonSchema(jsonSchema as JsonSchemaObject) as Schema;\n }\n\n // Return a basic schema if we can't identify the format\n return parameters as Schema;\n}\n\n/**\n * Checks if an object is a JSON schema\n */\nfunction isJsonSchema(obj: unknown): boolean {\n return (\n typeof obj === 'object' &&\n obj !== null &&\n ('type' in obj || 'properties' in obj || '$schema' in obj)\n );\n}\n\n/**\n * Checks if an object is a Zod schema\n */\nfunction isZodSchema(obj: unknown): obj is z.ZodTypeAny {\n return (\n typeof obj === 'object' &&\n obj !== null &&\n '_def' in obj &&\n typeof (obj as z.ZodTypeAny)._def === 'object'\n );\n}\n\n/**\n * Cleans JSON schema for Gemini compatibility\n * Removes $schema and other metadata that Gemini doesn't support\n */\nfunction cleanJsonSchema(schema: JsonSchemaObject): JsonSchemaObject {\n if (typeof schema !== 'object' || schema === null) {\n return schema;\n }\n\n const cleaned = { ...schema };\n\n // Remove $schema property\n delete cleaned.$schema;\n delete cleaned.$ref;\n delete cleaned.$defs;\n delete cleaned.definitions;\n\n // Recursively clean nested schemas\n if (cleaned.properties && typeof cleaned.properties === 'object') {\n const cleanedProps: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(cleaned.properties)) {\n cleanedProps[key] = cleanJsonSchema(value as JsonSchemaObject);\n }\n cleaned.properties = cleanedProps;\n }\n\n if (cleaned.items) {\n cleaned.items = cleanJsonSchema(cleaned.items as JsonSchemaObject);\n }\n\n if (\n cleaned.additionalProperties &&\n typeof cleaned.additionalProperties === 'object'\n ) {\n cleaned.additionalProperties = cleanJsonSchema(\n cleaned.additionalProperties as JsonSchemaObject\n );\n }\n\n // Clean arrays\n for (const key of ['allOf', 'anyOf', 'oneOf'] as const) {\n const arrayProp = cleaned[key];\n if (Array.isArray(arrayProp)) {\n cleaned[key] = arrayProp.map((item) =>\n cleanJsonSchema(item as JsonSchemaObject)\n );\n }\n }\n\n if (cleaned.properties && cleaned.type === undefined) {\n cleaned.type = 'object';\n }\n\n return cleaned;\n}\n\n/**\n * Maps Vercel AI SDK tool config options to Gemini format\n */\nexport function mapGeminiToolConfig(\n options: LanguageModelV3CallOptions\n): ToolConfig | undefined {\n if (options.toolChoice) {\n // Restrict allowed function names when a specific tool is forced.\n // Gemini expects that when forcing a tool call, the function name is\n // provided via `allowedFunctionNa