diff --git a/packages/@anthropic-ai/model-provider/src/providers/gemini/convertMessages.ts b/packages/@anthropic-ai/model-provider/src/providers/gemini/convertMessages.ts new file mode 100644 index 000000000..4b7acdb62 --- /dev/null +++ b/packages/@anthropic-ai/model-provider/src/providers/gemini/convertMessages.ts @@ -0,0 +1,307 @@ +import type { + BetaToolResultBlockParam, + BetaToolUseBlock, +} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' +import type { AssistantMessage, UserMessage } from '../../types/message.js' +import type { SystemPrompt } from '../../types/systemPrompt.js' +import { + GEMINI_THOUGHT_SIGNATURE_FIELD, + type GeminiContent, + type GeminiGenerateContentRequest, + type GeminiPart, +} from './types.js' + +// Simple JSON parse utility (replaces safeParseJSON from main project) +function safeParseJSON(json: string | null | undefined): unknown { + if (!json) return null + try { + return JSON.parse(json) + } catch { + return null + } +} + +export function anthropicMessagesToGemini( + messages: (UserMessage | AssistantMessage)[], + systemPrompt: SystemPrompt, +): Pick { + const contents: GeminiContent[] = [] + const toolNamesById = new Map() + + for (const msg of messages) { + if (msg.type === 'assistant') { + const content = convertInternalAssistantMessage(msg) + if (content.parts.length > 0) { + contents.push(content) + } + + const assistantContent = msg.message.content + if (Array.isArray(assistantContent)) { + for (const block of assistantContent) { + if (typeof block !== 'string' && block.type === 'tool_use') { + toolNamesById.set(block.id, block.name) + } + } + } + continue + } + + if (msg.type === 'user') { + const content = convertInternalUserMessage(msg, toolNamesById) + if (content.parts.length > 0) { + contents.push(content) + } + } + } + + const systemText = systemPromptToText(systemPrompt) + + return { + contents, + ...(systemText + ? { + systemInstruction: { + parts: [{ text: systemText }], + }, + } + : {}), + } +} + +function systemPromptToText(systemPrompt: SystemPrompt): string { + if (!systemPrompt || systemPrompt.length === 0) return '' + return systemPrompt.filter(Boolean).join('\n\n') +} + +function convertInternalUserMessage( + msg: UserMessage, + toolNamesById: ReadonlyMap, +): GeminiContent { + const content = msg.message.content + + if (typeof content === 'string') { + return { + role: 'user', + parts: createTextGeminiParts(content), + } + } + + if (!Array.isArray(content)) { + return { role: 'user', parts: [] } + } + + return { + role: 'user', + parts: content.flatMap(block => + convertUserContentBlockToGeminiParts(block as unknown as string | Record, toolNamesById), + ), + } +} + +function convertUserContentBlockToGeminiParts( + block: string | Record, + toolNamesById: ReadonlyMap, +): GeminiPart[] { + if (typeof block === 'string') { + return createTextGeminiParts(block) + } + + if (block.type === 'text') { + return createTextGeminiParts(block.text) + } + + if (block.type === 'tool_result') { + const toolResult = block as unknown as BetaToolResultBlockParam + return [ + { + functionResponse: { + name: toolNamesById.get(toolResult.tool_use_id) ?? toolResult.tool_use_id, + response: toolResultToResponseObject(toolResult), + }, + }, + ] + } + + // Convert Anthropic image blocks to Gemini inlineData + if (block.type === 'image') { + const source = block.source as Record | undefined + if (source?.type === 'base64' && typeof source.data === 'string') { + const mediaType = (source.media_type as string) || 'image/png' + return [ + { + inlineData: { + mimeType: mediaType, + data: source.data, + }, + }, + ] + } + // URL images not directly supported by Gemini, convert to text description + if (source?.type === 'url' && typeof source.url === 'string') { + return createTextGeminiParts(`[image: ${source.url}]`) + } + } + + return [] +} + +function convertInternalAssistantMessage(msg: AssistantMessage): GeminiContent { + const content = msg.message.content + + if (typeof content === 'string') { + return { + role: 'model', + parts: createTextGeminiParts(content), + } + } + + if (!Array.isArray(content)) { + return { role: 'model', parts: [] } + } + + const parts: GeminiPart[] = [] + for (const block of content) { + if (typeof block === 'string') { + parts.push(...createTextGeminiParts(block)) + continue + } + + if (block.type === 'text') { + parts.push( + ...createTextGeminiParts( + block.text, + getGeminiThoughtSignature(block as unknown as Record), + ), + ) + continue + } + + if (block.type === 'thinking') { + const thinkingPart = createThinkingGeminiPart( + block.thinking, + block.signature, + ) + if (thinkingPart) { + parts.push(thinkingPart) + } + continue + } + + if (block.type === 'tool_use') { + const toolUse = block as unknown as BetaToolUseBlock + parts.push({ + functionCall: { + name: toolUse.name, + args: normalizeToolUseInput(toolUse.input), + }, + ...(getGeminiThoughtSignature(block as unknown as Record) && { + thoughtSignature: getGeminiThoughtSignature(block as unknown as Record), + }), + }) + } + } + + return { role: 'model', parts } +} + +function createTextGeminiParts( + value: unknown, + thoughtSignature?: string, +): GeminiPart[] { + if (typeof value !== 'string' || value.length === 0) { + return [] + } + + return [ + { + text: value, + ...(thoughtSignature && { thoughtSignature }), + }, + ] +} + +function createThinkingGeminiPart( + value: unknown, + thoughtSignature?: string, +): GeminiPart | undefined { + if (typeof value !== 'string' || value.length === 0) { + return undefined + } + + return { + text: value, + thought: true, + ...(thoughtSignature && { thoughtSignature }), + } +} + +function normalizeToolUseInput(input: unknown): Record { + if (typeof input === 'string') { + const parsed = safeParseJSON(input) + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + return parsed as Record + } + return parsed === null ? {} : { value: parsed } + } + + if (input && typeof input === 'object' && !Array.isArray(input)) { + return input as Record + } + + return input === undefined ? {} : { value: input } +} + +function toolResultToResponseObject( + block: BetaToolResultBlockParam, +): Record { + const result = normalizeToolResultContent(block.content) + if ( + result && + typeof result === 'object' && + !Array.isArray(result) + ) { + return block.is_error ? { ...(result as Record), is_error: true } : result as Record + } + + return { + result, + ...(block.is_error ? { is_error: true } : {}), + } +} + +function normalizeToolResultContent(content: unknown): unknown { + if (typeof content === 'string') { + const parsed = safeParseJSON(content) + return parsed ?? content + } + + if (Array.isArray(content)) { + const text = content + .map(part => { + if (typeof part === 'string') return part + if ( + part && + typeof part === 'object' && + 'text' in part && + typeof part.text === 'string' + ) { + return part.text + } + return '' + }) + .filter(Boolean) + .join('\n') + + const parsed = safeParseJSON(text) + return parsed ?? text + } + + return content ?? '' +} + +function getGeminiThoughtSignature(block: Record): string | undefined { + const signature = block[GEMINI_THOUGHT_SIGNATURE_FIELD] + return typeof signature === 'string' && signature.length > 0 + ? signature + : undefined +} diff --git a/packages/@anthropic-ai/model-provider/src/providers/gemini/convertTools.ts b/packages/@anthropic-ai/model-provider/src/providers/gemini/convertTools.ts new file mode 100644 index 000000000..7f6fc82c5 --- /dev/null +++ b/packages/@anthropic-ai/model-provider/src/providers/gemini/convertTools.ts @@ -0,0 +1,285 @@ +import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' +import type { + GeminiFunctionCallingConfig, + GeminiTool, +} from './types.js' + +const GEMINI_JSON_SCHEMA_TYPES = new Set([ + 'string', + 'number', + 'integer', + 'boolean', + 'object', + 'array', + 'null', +]) + +function normalizeGeminiJsonSchemaType( + value: unknown, +): string | string[] | undefined { + if (typeof value === 'string') { + return GEMINI_JSON_SCHEMA_TYPES.has(value) ? value : undefined + } + + if (Array.isArray(value)) { + const normalized = value.filter( + (item): item is string => + typeof item === 'string' && GEMINI_JSON_SCHEMA_TYPES.has(item), + ) + const unique = Array.from(new Set(normalized)) + if (unique.length === 0) return undefined + return unique.length === 1 ? unique[0] : unique + } + + return undefined +} + +function inferGeminiJsonSchemaTypeFromValue(value: unknown): string | undefined { + if (value === null) return 'null' + if (Array.isArray(value)) return 'array' + if (typeof value === 'string') return 'string' + if (typeof value === 'boolean') return 'boolean' + if (typeof value === 'number') { + return Number.isInteger(value) ? 'integer' : 'number' + } + if (typeof value === 'object') return 'object' + return undefined +} + +function inferGeminiJsonSchemaTypeFromEnum( + values: unknown[], +): string | string[] | undefined { + const inferred = values + .map(inferGeminiJsonSchemaTypeFromValue) + .filter((value): value is string => value !== undefined) + const unique = Array.from(new Set(inferred)) + if (unique.length === 0) return undefined + return unique.length === 1 ? unique[0] : unique +} + +function addNullToGeminiJsonSchemaType( + value: string | string[] | undefined, +): string | string[] | undefined { + if (value === undefined) return ['null'] + if (Array.isArray(value)) { + return value.includes('null') ? value : [...value, 'null'] + } + return value === 'null' ? value : [value, 'null'] +} + +function sanitizeGeminiJsonSchemaProperties( + value: unknown, +): Record> | undefined { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return undefined + } + + const sanitizedEntries = Object.entries(value as Record) + .map(([key, schema]) => [key, sanitizeGeminiJsonSchema(schema)] as const) + .filter(([, schema]) => Object.keys(schema).length > 0) + + if (sanitizedEntries.length === 0) { + return undefined + } + + return Object.fromEntries(sanitizedEntries) +} + +function sanitizeGeminiJsonSchemaArray( + value: unknown, +): Record[] | undefined { + if (!Array.isArray(value)) return undefined + + const sanitized = value + .map(item => sanitizeGeminiJsonSchema(item)) + .filter(item => Object.keys(item).length > 0) + + return sanitized.length > 0 ? sanitized : undefined +} + +function sanitizeGeminiJsonSchema( + schema: unknown, +): Record { + if (!schema || typeof schema !== 'object' || Array.isArray(schema)) { + return {} + } + + const source = schema as Record + const result: Record = {} + + let type = normalizeGeminiJsonSchemaType(source.type) + + if (source.const !== undefined) { + result.enum = [source.const] + type = type ?? inferGeminiJsonSchemaTypeFromValue(source.const) + } else if (Array.isArray(source.enum) && source.enum.length > 0) { + result.enum = source.enum + type = type ?? inferGeminiJsonSchemaTypeFromEnum(source.enum) + } + + if (!type) { + if (source.properties && typeof source.properties === 'object') { + type = 'object' + } else if (source.items !== undefined || source.prefixItems !== undefined) { + type = 'array' + } + } + + if (source.nullable === true) { + type = addNullToGeminiJsonSchemaType(type) + } + + if (type) { + result.type = type + } + + if (typeof source.title === 'string') { + result.title = source.title + } + if (typeof source.description === 'string') { + result.description = source.description + } + if (typeof source.format === 'string') { + result.format = source.format + } + if (typeof source.pattern === 'string') { + result.pattern = source.pattern + } + if (typeof source.minimum === 'number') { + result.minimum = source.minimum + } else if (typeof source.exclusiveMinimum === 'number') { + result.minimum = source.exclusiveMinimum + } + if (typeof source.maximum === 'number') { + result.maximum = source.maximum + } else if (typeof source.exclusiveMaximum === 'number') { + result.maximum = source.exclusiveMaximum + } + if (typeof source.minItems === 'number') { + result.minItems = source.minItems + } + if (typeof source.maxItems === 'number') { + result.maxItems = source.maxItems + } + if (typeof source.minLength === 'number') { + result.minLength = source.minLength + } + if (typeof source.maxLength === 'number') { + result.maxLength = source.maxLength + } + if (typeof source.minProperties === 'number') { + result.minProperties = source.minProperties + } + if (typeof source.maxProperties === 'number') { + result.maxProperties = source.maxProperties + } + + const properties = sanitizeGeminiJsonSchemaProperties(source.properties) + if (properties) { + result.properties = properties + result.propertyOrdering = Object.keys(properties) + } + + if (Array.isArray(source.required)) { + const required = source.required.filter( + (item): item is string => typeof item === 'string', + ) + if (required.length > 0) { + result.required = required + } + } + + if (typeof source.additionalProperties === 'boolean') { + result.additionalProperties = source.additionalProperties + } else { + const additionalProperties = sanitizeGeminiJsonSchema( + source.additionalProperties, + ) + if (Object.keys(additionalProperties).length > 0) { + result.additionalProperties = additionalProperties + } + } + + const items = sanitizeGeminiJsonSchema(source.items) + if (Object.keys(items).length > 0) { + result.items = items + } + + const prefixItems = sanitizeGeminiJsonSchemaArray(source.prefixItems) + if (prefixItems) { + result.prefixItems = prefixItems + } + + const anyOf = sanitizeGeminiJsonSchemaArray(source.anyOf ?? source.oneOf) + if (anyOf) { + result.anyOf = anyOf + } + + return result +} + +function sanitizeGeminiFunctionParameters( + schema: unknown, +): Record { + const sanitized = sanitizeGeminiJsonSchema(schema) + if (Object.keys(sanitized).length > 0) { + return sanitized + } + + return { + type: 'object', + properties: {}, + } +} + +export function anthropicToolsToGemini(tools: BetaToolUnion[]): GeminiTool[] { + const functionDeclarations = tools + .filter(tool => { + const toolType = (tool as unknown as { type?: string }).type + return tool.type === 'custom' || !('type' in tool) || toolType !== 'server' + }) + .map(tool => { + const anyTool = tool as unknown as Record + const name = (anyTool.name as string) || '' + const description = (anyTool.description as string) || '' + const inputSchema = + (anyTool.input_schema as Record | undefined) ?? { + type: 'object', + properties: {}, + } + + return { + name, + description, + parametersJsonSchema: sanitizeGeminiFunctionParameters(inputSchema), + } + }) + + return functionDeclarations.length > 0 + ? [{ functionDeclarations }] + : [] +} + +export function anthropicToolChoiceToGemini( + toolChoice: unknown, +): GeminiFunctionCallingConfig | undefined { + if (!toolChoice || typeof toolChoice !== 'object') return undefined + + const tc = toolChoice as Record + const type = tc.type as string + + switch (type) { + case 'auto': + return { mode: 'AUTO' } + case 'any': + return { mode: 'ANY' } + case 'tool': + return { + mode: 'ANY', + allowedFunctionNames: + typeof tc.name === 'string' ? [tc.name] : undefined, + } + default: + return undefined + } +} diff --git a/packages/@anthropic-ai/model-provider/src/providers/gemini/modelMapping.ts b/packages/@anthropic-ai/model-provider/src/providers/gemini/modelMapping.ts new file mode 100644 index 000000000..19afae855 --- /dev/null +++ b/packages/@anthropic-ai/model-provider/src/providers/gemini/modelMapping.ts @@ -0,0 +1,35 @@ +function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null { + if (/haiku/i.test(model)) return 'haiku' + if (/opus/i.test(model)) return 'opus' + if (/sonnet/i.test(model)) return 'sonnet' + return null +} + +export function resolveGeminiModel(anthropicModel: string): string { + if (process.env.GEMINI_MODEL) { + return process.env.GEMINI_MODEL + } + + const cleanModel = anthropicModel.replace(/\[1m\]$/i, '') + const family = getModelFamily(cleanModel) + + if (!family) { + return cleanModel + } + + const geminiEnvVar = `GEMINI_DEFAULT_${family.toUpperCase()}_MODEL` + const geminiModel = process.env[geminiEnvVar] + if (geminiModel) { + return geminiModel + } + + const sharedEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL` + const resolvedModel = process.env[sharedEnvVar] + if (resolvedModel) { + return resolvedModel + } + + throw new Error( + `Gemini provider requires GEMINI_MODEL or ${geminiEnvVar} (or ${sharedEnvVar} for backward compatibility) to be configured.`, + ) +} diff --git a/packages/@anthropic-ai/model-provider/src/providers/gemini/streamAdapter.ts b/packages/@anthropic-ai/model-provider/src/providers/gemini/streamAdapter.ts new file mode 100644 index 000000000..d40980e04 --- /dev/null +++ b/packages/@anthropic-ai/model-provider/src/providers/gemini/streamAdapter.ts @@ -0,0 +1,243 @@ +import type { BetaRawMessageStreamEvent } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' +import { randomUUID } from 'crypto' +import type { GeminiPart, GeminiStreamChunk } from './types.js' + +export async function* adaptGeminiStreamToAnthropic( + stream: AsyncIterable, + model: string, +): AsyncGenerator { + const messageId = `msg_${randomUUID().replace(/-/g, '').slice(0, 24)}` + let started = false + let stopped = false + let nextContentIndex = 0 + let openTextLikeBlock: + | { index: number; type: 'text' | 'thinking' } + | null = null + let sawToolUse = false + let finishReason: string | undefined + let inputTokens = 0 + let outputTokens = 0 + + for await (const chunk of stream) { + const usage = chunk.usageMetadata + if (usage) { + inputTokens = usage.promptTokenCount ?? inputTokens + outputTokens = + (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0) + } + + if (!started) { + started = true + yield { + type: 'message_start', + message: { + id: messageId, + type: 'message', + role: 'assistant', + content: [], + model, + stop_reason: null, + stop_sequence: null, + usage: { + input_tokens: inputTokens, + output_tokens: 0, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + }, + } as unknown as BetaRawMessageStreamEvent + } + const candidate = chunk.candidates?.[0] + const parts = candidate?.content?.parts ?? [] + + for (const part of parts) { + if (part.functionCall) { + if (openTextLikeBlock) { + yield { + type: 'content_block_stop', + index: openTextLikeBlock.index, + } as BetaRawMessageStreamEvent + openTextLikeBlock = null + } + + sawToolUse = true + const toolIndex = nextContentIndex++ + const toolId = `toolu_${randomUUID().replace(/-/g, '').slice(0, 24)}` + yield { + type: 'content_block_start', + index: toolIndex, + content_block: { + type: 'tool_use', + id: toolId, + name: part.functionCall.name || '', + input: {}, + }, + } as BetaRawMessageStreamEvent + + if (part.thoughtSignature) { + yield { + type: 'content_block_delta', + index: toolIndex, + delta: { + type: 'signature_delta', + signature: part.thoughtSignature, + }, + } as BetaRawMessageStreamEvent + } + + if (part.functionCall.args && Object.keys(part.functionCall.args).length > 0) { + yield { + type: 'content_block_delta', + index: toolIndex, + delta: { + type: 'input_json_delta', + partial_json: JSON.stringify(part.functionCall.args), + }, + } as BetaRawMessageStreamEvent + } + + yield { + type: 'content_block_stop', + index: toolIndex, + } as BetaRawMessageStreamEvent + continue + } + + const textLikeType = getTextLikeBlockType(part) + if (textLikeType) { + if (!openTextLikeBlock || openTextLikeBlock.type !== textLikeType) { + if (openTextLikeBlock) { + yield { + type: 'content_block_stop', + index: openTextLikeBlock.index, + } as BetaRawMessageStreamEvent + } + + openTextLikeBlock = { + index: nextContentIndex++, + type: textLikeType, + } + + yield { + type: 'content_block_start', + index: openTextLikeBlock.index, + content_block: + textLikeType === 'thinking' + ? { + type: 'thinking', + thinking: '', + signature: '', + } + : { + type: 'text', + text: '', + }, + } as BetaRawMessageStreamEvent + } + + if (part.text) { + yield { + type: 'content_block_delta', + index: openTextLikeBlock.index, + delta: + textLikeType === 'thinking' + ? { + type: 'thinking_delta', + thinking: part.text, + } + : { + type: 'text_delta', + text: part.text, + }, + } as BetaRawMessageStreamEvent + } + + if (part.thoughtSignature) { + yield { + type: 'content_block_delta', + index: openTextLikeBlock.index, + delta: { + type: 'signature_delta', + signature: part.thoughtSignature, + }, + } as BetaRawMessageStreamEvent + } + + continue + } + + if (part.thoughtSignature && openTextLikeBlock) { + yield { + type: 'content_block_delta', + index: openTextLikeBlock.index, + delta: { + type: 'signature_delta', + signature: part.thoughtSignature, + }, + } as BetaRawMessageStreamEvent + } + } + + if (candidate?.finishReason) { + finishReason = candidate.finishReason + } + } + + if (!started) { + return + } + + if (openTextLikeBlock) { + yield { + type: 'content_block_stop', + index: openTextLikeBlock.index, + } as BetaRawMessageStreamEvent + } + + if (!stopped) { + yield { + type: 'message_delta', + delta: { + stop_reason: mapGeminiFinishReason(finishReason, sawToolUse), + stop_sequence: null, + }, + usage: { + output_tokens: outputTokens, + }, + } as BetaRawMessageStreamEvent + + yield { + type: 'message_stop', + } as BetaRawMessageStreamEvent + stopped = true + } +} + +function getTextLikeBlockType( + part: GeminiPart, +): 'text' | 'thinking' | null { + if (typeof part.text !== 'string') { + return null + } + return part.thought ? 'thinking' : 'text' +} + +function mapGeminiFinishReason( + reason: string | undefined, + sawToolUse: boolean, +): string { + switch (reason) { + case 'MAX_TOKENS': + return 'max_tokens' + case 'STOP': + case 'FINISH_REASON_UNSPECIFIED': + case 'SAFETY': + case 'RECITATION': + case 'BLOCKLIST': + case 'PROHIBITED_CONTENT': + case 'SPII': + case 'MALFORMED_FUNCTION_CALL': + default: + return sawToolUse ? 'tool_use' : 'end_turn' + } +} diff --git a/packages/@anthropic-ai/model-provider/src/providers/gemini/types.ts b/packages/@anthropic-ai/model-provider/src/providers/gemini/types.ts new file mode 100644 index 000000000..e8718fecd --- /dev/null +++ b/packages/@anthropic-ai/model-provider/src/providers/gemini/types.ts @@ -0,0 +1,86 @@ +export const GEMINI_THOUGHT_SIGNATURE_FIELD = '_geminiThoughtSignature' + +export type GeminiFunctionCall = { + name?: string + args?: Record +} + +export type GeminiFunctionResponse = { + name?: string + response?: Record +} + +export type GeminiInlineData = { + mimeType: string + data: string +} + +export type GeminiPart = { + text?: string + thought?: boolean + thoughtSignature?: string + functionCall?: GeminiFunctionCall + functionResponse?: GeminiFunctionResponse + inlineData?: GeminiInlineData +} + +export type GeminiContent = { + role: 'user' | 'model' + parts: GeminiPart[] +} + +export type GeminiFunctionDeclaration = { + name: string + description?: string + parameters?: Record + parametersJsonSchema?: Record +} + +export type GeminiTool = { + functionDeclarations: GeminiFunctionDeclaration[] +} + +export type GeminiFunctionCallingConfig = { + mode: 'AUTO' | 'ANY' | 'NONE' + allowedFunctionNames?: string[] +} + +export type GeminiGenerateContentRequest = { + contents: GeminiContent[] + systemInstruction?: { + parts: Array<{ text: string }> + } + tools?: GeminiTool[] + toolConfig?: { + functionCallingConfig: GeminiFunctionCallingConfig + } + generationConfig?: { + temperature?: number + thinkingConfig?: { + includeThoughts?: boolean + thinkingBudget?: number + } + } +} + +export type GeminiUsageMetadata = { + promptTokenCount?: number + candidatesTokenCount?: number + thoughtsTokenCount?: number + totalTokenCount?: number +} + +export type GeminiCandidate = { + content?: { + role?: string + parts?: GeminiPart[] + } + finishReason?: string + index?: number +} + +export type GeminiStreamChunk = { + candidates?: GeminiCandidate[] + usageMetadata?: GeminiUsageMetadata + modelVersion?: string +} diff --git a/src/services/api/gemini/convertMessages.ts b/src/services/api/gemini/convertMessages.ts index 0bdf22223..a31c2f023 100644 --- a/src/services/api/gemini/convertMessages.ts +++ b/src/services/api/gemini/convertMessages.ts @@ -1,298 +1,2 @@ -import type { - BetaToolResultBlockParam, - BetaToolUseBlock, -} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' -import type { AssistantMessage, UserMessage } from '../../../types/message.js' -import { safeParseJSON } from '../../../utils/json.js' -import type { SystemPrompt } from '../../../utils/systemPromptType.js' -import { - GEMINI_THOUGHT_SIGNATURE_FIELD, - type GeminiContent, - type GeminiGenerateContentRequest, - type GeminiPart, -} from './types.js' - -export function anthropicMessagesToGemini( - messages: (UserMessage | AssistantMessage)[], - systemPrompt: SystemPrompt, -): Pick { - const contents: GeminiContent[] = [] - const toolNamesById = new Map() - - for (const msg of messages) { - if (msg.type === 'assistant') { - const content = convertInternalAssistantMessage(msg) - if (content.parts.length > 0) { - contents.push(content) - } - - const assistantContent = msg.message.content - if (Array.isArray(assistantContent)) { - for (const block of assistantContent) { - if (typeof block !== 'string' && block.type === 'tool_use') { - toolNamesById.set(block.id, block.name) - } - } - } - continue - } - - if (msg.type === 'user') { - const content = convertInternalUserMessage(msg, toolNamesById) - if (content.parts.length > 0) { - contents.push(content) - } - } - } - - const systemText = systemPromptToText(systemPrompt) - - return { - contents, - ...(systemText - ? { - systemInstruction: { - parts: [{ text: systemText }], - }, - } - : {}), - } -} - -function systemPromptToText(systemPrompt: SystemPrompt): string { - if (!systemPrompt || systemPrompt.length === 0) return '' - return systemPrompt.filter(Boolean).join('\n\n') -} - -function convertInternalUserMessage( - msg: UserMessage, - toolNamesById: ReadonlyMap, -): GeminiContent { - const content = msg.message.content - - if (typeof content === 'string') { - return { - role: 'user', - parts: createTextGeminiParts(content), - } - } - - if (!Array.isArray(content)) { - return { role: 'user', parts: [] } - } - - return { - role: 'user', - parts: content.flatMap(block => - convertUserContentBlockToGeminiParts(block as unknown as string | Record, toolNamesById), - ), - } -} - -function convertUserContentBlockToGeminiParts( - block: string | Record, - toolNamesById: ReadonlyMap, -): GeminiPart[] { - if (typeof block === 'string') { - return createTextGeminiParts(block) - } - - if (block.type === 'text') { - return createTextGeminiParts(block.text) - } - - if (block.type === 'tool_result') { - const toolResult = block as unknown as BetaToolResultBlockParam - return [ - { - functionResponse: { - name: toolNamesById.get(toolResult.tool_use_id) ?? toolResult.tool_use_id, - response: toolResultToResponseObject(toolResult), - }, - }, - ] - } - - // 将 Anthropic image 块转换为 Gemini inlineData - if (block.type === 'image') { - const source = block.source as Record | undefined - if (source?.type === 'base64' && typeof source.data === 'string') { - const mediaType = (source.media_type as string) || 'image/png' - return [ - { - inlineData: { - mimeType: mediaType, - data: source.data, - }, - }, - ] - } - // url 类型的图片,Gemini 不直接支持,转为文本描述 - if (source?.type === 'url' && typeof source.url === 'string') { - return createTextGeminiParts(`[image: ${source.url}]`) - } - } - - return [] -} - -function convertInternalAssistantMessage(msg: AssistantMessage): GeminiContent { - const content = msg.message.content - - if (typeof content === 'string') { - return { - role: 'model', - parts: createTextGeminiParts(content), - } - } - - if (!Array.isArray(content)) { - return { role: 'model', parts: [] } - } - - const parts: GeminiPart[] = [] - for (const block of content) { - if (typeof block === 'string') { - parts.push(...createTextGeminiParts(block)) - continue - } - - if (block.type === 'text') { - parts.push( - ...createTextGeminiParts( - block.text, - getGeminiThoughtSignature(block as unknown as Record), - ), - ) - continue - } - - if (block.type === 'thinking') { - const thinkingPart = createThinkingGeminiPart( - block.thinking, - block.signature, - ) - if (thinkingPart) { - parts.push(thinkingPart) - } - continue - } - - if (block.type === 'tool_use') { - const toolUse = block as unknown as BetaToolUseBlock - parts.push({ - functionCall: { - name: toolUse.name, - args: normalizeToolUseInput(toolUse.input), - }, - ...(getGeminiThoughtSignature(block as unknown as Record) && { - thoughtSignature: getGeminiThoughtSignature(block as unknown as Record), - }), - }) - } - } - - return { role: 'model', parts } -} - -function createTextGeminiParts( - value: unknown, - thoughtSignature?: string, -): GeminiPart[] { - if (typeof value !== 'string' || value.length === 0) { - return [] - } - - return [ - { - text: value, - ...(thoughtSignature && { thoughtSignature }), - }, - ] -} - -function createThinkingGeminiPart( - value: unknown, - thoughtSignature?: string, -): GeminiPart | undefined { - if (typeof value !== 'string' || value.length === 0) { - return undefined - } - - return { - text: value, - thought: true, - ...(thoughtSignature && { thoughtSignature }), - } -} - -function normalizeToolUseInput(input: unknown): Record { - if (typeof input === 'string') { - const parsed = safeParseJSON(input) - if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { - return parsed as Record - } - return parsed === null ? {} : { value: parsed } - } - - if (input && typeof input === 'object' && !Array.isArray(input)) { - return input as Record - } - - return input === undefined ? {} : { value: input } -} - -function toolResultToResponseObject( - block: BetaToolResultBlockParam, -): Record { - const result = normalizeToolResultContent(block.content) - if ( - result && - typeof result === 'object' && - !Array.isArray(result) - ) { - return block.is_error ? { ...(result as Record), is_error: true } : result as Record - } - - return { - result, - ...(block.is_error ? { is_error: true } : {}), - } -} - -function normalizeToolResultContent(content: unknown): unknown { - if (typeof content === 'string') { - const parsed = safeParseJSON(content) - return parsed ?? content - } - - if (Array.isArray(content)) { - const text = content - .map(part => { - if (typeof part === 'string') return part - if ( - part && - typeof part === 'object' && - 'text' in part && - typeof part.text === 'string' - ) { - return part.text - } - return '' - }) - .filter(Boolean) - .join('\n') - - const parsed = safeParseJSON(text) - return parsed ?? text - } - - return content ?? '' -} - -function getGeminiThoughtSignature(block: Record): string | undefined { - const signature = block[GEMINI_THOUGHT_SIGNATURE_FIELD] - return typeof signature === 'string' && signature.length > 0 - ? signature - : undefined -} +// Re-export from @anthropic-ai/model-provider +export { anthropicMessagesToGemini } from '@anthropic-ai/model-provider' diff --git a/src/services/api/gemini/convertTools.ts b/src/services/api/gemini/convertTools.ts index 7f6fc82c5..c2bb3bf3f 100644 --- a/src/services/api/gemini/convertTools.ts +++ b/src/services/api/gemini/convertTools.ts @@ -1,285 +1,2 @@ -import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' -import type { - GeminiFunctionCallingConfig, - GeminiTool, -} from './types.js' - -const GEMINI_JSON_SCHEMA_TYPES = new Set([ - 'string', - 'number', - 'integer', - 'boolean', - 'object', - 'array', - 'null', -]) - -function normalizeGeminiJsonSchemaType( - value: unknown, -): string | string[] | undefined { - if (typeof value === 'string') { - return GEMINI_JSON_SCHEMA_TYPES.has(value) ? value : undefined - } - - if (Array.isArray(value)) { - const normalized = value.filter( - (item): item is string => - typeof item === 'string' && GEMINI_JSON_SCHEMA_TYPES.has(item), - ) - const unique = Array.from(new Set(normalized)) - if (unique.length === 0) return undefined - return unique.length === 1 ? unique[0] : unique - } - - return undefined -} - -function inferGeminiJsonSchemaTypeFromValue(value: unknown): string | undefined { - if (value === null) return 'null' - if (Array.isArray(value)) return 'array' - if (typeof value === 'string') return 'string' - if (typeof value === 'boolean') return 'boolean' - if (typeof value === 'number') { - return Number.isInteger(value) ? 'integer' : 'number' - } - if (typeof value === 'object') return 'object' - return undefined -} - -function inferGeminiJsonSchemaTypeFromEnum( - values: unknown[], -): string | string[] | undefined { - const inferred = values - .map(inferGeminiJsonSchemaTypeFromValue) - .filter((value): value is string => value !== undefined) - const unique = Array.from(new Set(inferred)) - if (unique.length === 0) return undefined - return unique.length === 1 ? unique[0] : unique -} - -function addNullToGeminiJsonSchemaType( - value: string | string[] | undefined, -): string | string[] | undefined { - if (value === undefined) return ['null'] - if (Array.isArray(value)) { - return value.includes('null') ? value : [...value, 'null'] - } - return value === 'null' ? value : [value, 'null'] -} - -function sanitizeGeminiJsonSchemaProperties( - value: unknown, -): Record> | undefined { - if (!value || typeof value !== 'object' || Array.isArray(value)) { - return undefined - } - - const sanitizedEntries = Object.entries(value as Record) - .map(([key, schema]) => [key, sanitizeGeminiJsonSchema(schema)] as const) - .filter(([, schema]) => Object.keys(schema).length > 0) - - if (sanitizedEntries.length === 0) { - return undefined - } - - return Object.fromEntries(sanitizedEntries) -} - -function sanitizeGeminiJsonSchemaArray( - value: unknown, -): Record[] | undefined { - if (!Array.isArray(value)) return undefined - - const sanitized = value - .map(item => sanitizeGeminiJsonSchema(item)) - .filter(item => Object.keys(item).length > 0) - - return sanitized.length > 0 ? sanitized : undefined -} - -function sanitizeGeminiJsonSchema( - schema: unknown, -): Record { - if (!schema || typeof schema !== 'object' || Array.isArray(schema)) { - return {} - } - - const source = schema as Record - const result: Record = {} - - let type = normalizeGeminiJsonSchemaType(source.type) - - if (source.const !== undefined) { - result.enum = [source.const] - type = type ?? inferGeminiJsonSchemaTypeFromValue(source.const) - } else if (Array.isArray(source.enum) && source.enum.length > 0) { - result.enum = source.enum - type = type ?? inferGeminiJsonSchemaTypeFromEnum(source.enum) - } - - if (!type) { - if (source.properties && typeof source.properties === 'object') { - type = 'object' - } else if (source.items !== undefined || source.prefixItems !== undefined) { - type = 'array' - } - } - - if (source.nullable === true) { - type = addNullToGeminiJsonSchemaType(type) - } - - if (type) { - result.type = type - } - - if (typeof source.title === 'string') { - result.title = source.title - } - if (typeof source.description === 'string') { - result.description = source.description - } - if (typeof source.format === 'string') { - result.format = source.format - } - if (typeof source.pattern === 'string') { - result.pattern = source.pattern - } - if (typeof source.minimum === 'number') { - result.minimum = source.minimum - } else if (typeof source.exclusiveMinimum === 'number') { - result.minimum = source.exclusiveMinimum - } - if (typeof source.maximum === 'number') { - result.maximum = source.maximum - } else if (typeof source.exclusiveMaximum === 'number') { - result.maximum = source.exclusiveMaximum - } - if (typeof source.minItems === 'number') { - result.minItems = source.minItems - } - if (typeof source.maxItems === 'number') { - result.maxItems = source.maxItems - } - if (typeof source.minLength === 'number') { - result.minLength = source.minLength - } - if (typeof source.maxLength === 'number') { - result.maxLength = source.maxLength - } - if (typeof source.minProperties === 'number') { - result.minProperties = source.minProperties - } - if (typeof source.maxProperties === 'number') { - result.maxProperties = source.maxProperties - } - - const properties = sanitizeGeminiJsonSchemaProperties(source.properties) - if (properties) { - result.properties = properties - result.propertyOrdering = Object.keys(properties) - } - - if (Array.isArray(source.required)) { - const required = source.required.filter( - (item): item is string => typeof item === 'string', - ) - if (required.length > 0) { - result.required = required - } - } - - if (typeof source.additionalProperties === 'boolean') { - result.additionalProperties = source.additionalProperties - } else { - const additionalProperties = sanitizeGeminiJsonSchema( - source.additionalProperties, - ) - if (Object.keys(additionalProperties).length > 0) { - result.additionalProperties = additionalProperties - } - } - - const items = sanitizeGeminiJsonSchema(source.items) - if (Object.keys(items).length > 0) { - result.items = items - } - - const prefixItems = sanitizeGeminiJsonSchemaArray(source.prefixItems) - if (prefixItems) { - result.prefixItems = prefixItems - } - - const anyOf = sanitizeGeminiJsonSchemaArray(source.anyOf ?? source.oneOf) - if (anyOf) { - result.anyOf = anyOf - } - - return result -} - -function sanitizeGeminiFunctionParameters( - schema: unknown, -): Record { - const sanitized = sanitizeGeminiJsonSchema(schema) - if (Object.keys(sanitized).length > 0) { - return sanitized - } - - return { - type: 'object', - properties: {}, - } -} - -export function anthropicToolsToGemini(tools: BetaToolUnion[]): GeminiTool[] { - const functionDeclarations = tools - .filter(tool => { - const toolType = (tool as unknown as { type?: string }).type - return tool.type === 'custom' || !('type' in tool) || toolType !== 'server' - }) - .map(tool => { - const anyTool = tool as unknown as Record - const name = (anyTool.name as string) || '' - const description = (anyTool.description as string) || '' - const inputSchema = - (anyTool.input_schema as Record | undefined) ?? { - type: 'object', - properties: {}, - } - - return { - name, - description, - parametersJsonSchema: sanitizeGeminiFunctionParameters(inputSchema), - } - }) - - return functionDeclarations.length > 0 - ? [{ functionDeclarations }] - : [] -} - -export function anthropicToolChoiceToGemini( - toolChoice: unknown, -): GeminiFunctionCallingConfig | undefined { - if (!toolChoice || typeof toolChoice !== 'object') return undefined - - const tc = toolChoice as Record - const type = tc.type as string - - switch (type) { - case 'auto': - return { mode: 'AUTO' } - case 'any': - return { mode: 'ANY' } - case 'tool': - return { - mode: 'ANY', - allowedFunctionNames: - typeof tc.name === 'string' ? [tc.name] : undefined, - } - default: - return undefined - } -} +// Re-export from @anthropic-ai/model-provider +export { anthropicToolsToGemini, anthropicToolChoiceToGemini } from '@anthropic-ai/model-provider' diff --git a/src/services/api/gemini/modelMapping.ts b/src/services/api/gemini/modelMapping.ts index 1d372e026..132f2ba94 100644 --- a/src/services/api/gemini/modelMapping.ts +++ b/src/services/api/gemini/modelMapping.ts @@ -1,37 +1,2 @@ -function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null { - if (/haiku/i.test(model)) return 'haiku' - if (/opus/i.test(model)) return 'opus' - if (/sonnet/i.test(model)) return 'sonnet' - return null -} - -export function resolveGeminiModel(anthropicModel: string): string { - if (process.env.GEMINI_MODEL) { - return process.env.GEMINI_MODEL - } - - const cleanModel = anthropicModel.replace(/\[1m\]$/i, '') - const family = getModelFamily(cleanModel) - - if (!family) { - return cleanModel - } - - // First, try Gemini-specific DEFAULT variables (separated from Anthropic) - const geminiEnvVar = `GEMINI_DEFAULT_${family.toUpperCase()}_MODEL` - const geminiModel = process.env[geminiEnvVar] - if (geminiModel) { - return geminiModel - } - - // Fallback to Anthropic DEFAULT variables for backward compatibility - const sharedEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL` - const resolvedModel = process.env[sharedEnvVar] - if (resolvedModel) { - return resolvedModel - } - - throw new Error( - `Gemini provider requires GEMINI_MODEL or ${geminiEnvVar} (or ${sharedEnvVar} for backward compatibility) to be configured.`, - ) -} +// Re-export from @anthropic-ai/model-provider +export { resolveGeminiModel } from '@anthropic-ai/model-provider' diff --git a/src/services/api/gemini/streamAdapter.ts b/src/services/api/gemini/streamAdapter.ts index d40980e04..07c94c391 100644 --- a/src/services/api/gemini/streamAdapter.ts +++ b/src/services/api/gemini/streamAdapter.ts @@ -1,243 +1,2 @@ -import type { BetaRawMessageStreamEvent } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' -import { randomUUID } from 'crypto' -import type { GeminiPart, GeminiStreamChunk } from './types.js' - -export async function* adaptGeminiStreamToAnthropic( - stream: AsyncIterable, - model: string, -): AsyncGenerator { - const messageId = `msg_${randomUUID().replace(/-/g, '').slice(0, 24)}` - let started = false - let stopped = false - let nextContentIndex = 0 - let openTextLikeBlock: - | { index: number; type: 'text' | 'thinking' } - | null = null - let sawToolUse = false - let finishReason: string | undefined - let inputTokens = 0 - let outputTokens = 0 - - for await (const chunk of stream) { - const usage = chunk.usageMetadata - if (usage) { - inputTokens = usage.promptTokenCount ?? inputTokens - outputTokens = - (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0) - } - - if (!started) { - started = true - yield { - type: 'message_start', - message: { - id: messageId, - type: 'message', - role: 'assistant', - content: [], - model, - stop_reason: null, - stop_sequence: null, - usage: { - input_tokens: inputTokens, - output_tokens: 0, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0, - }, - }, - } as unknown as BetaRawMessageStreamEvent - } - const candidate = chunk.candidates?.[0] - const parts = candidate?.content?.parts ?? [] - - for (const part of parts) { - if (part.functionCall) { - if (openTextLikeBlock) { - yield { - type: 'content_block_stop', - index: openTextLikeBlock.index, - } as BetaRawMessageStreamEvent - openTextLikeBlock = null - } - - sawToolUse = true - const toolIndex = nextContentIndex++ - const toolId = `toolu_${randomUUID().replace(/-/g, '').slice(0, 24)}` - yield { - type: 'content_block_start', - index: toolIndex, - content_block: { - type: 'tool_use', - id: toolId, - name: part.functionCall.name || '', - input: {}, - }, - } as BetaRawMessageStreamEvent - - if (part.thoughtSignature) { - yield { - type: 'content_block_delta', - index: toolIndex, - delta: { - type: 'signature_delta', - signature: part.thoughtSignature, - }, - } as BetaRawMessageStreamEvent - } - - if (part.functionCall.args && Object.keys(part.functionCall.args).length > 0) { - yield { - type: 'content_block_delta', - index: toolIndex, - delta: { - type: 'input_json_delta', - partial_json: JSON.stringify(part.functionCall.args), - }, - } as BetaRawMessageStreamEvent - } - - yield { - type: 'content_block_stop', - index: toolIndex, - } as BetaRawMessageStreamEvent - continue - } - - const textLikeType = getTextLikeBlockType(part) - if (textLikeType) { - if (!openTextLikeBlock || openTextLikeBlock.type !== textLikeType) { - if (openTextLikeBlock) { - yield { - type: 'content_block_stop', - index: openTextLikeBlock.index, - } as BetaRawMessageStreamEvent - } - - openTextLikeBlock = { - index: nextContentIndex++, - type: textLikeType, - } - - yield { - type: 'content_block_start', - index: openTextLikeBlock.index, - content_block: - textLikeType === 'thinking' - ? { - type: 'thinking', - thinking: '', - signature: '', - } - : { - type: 'text', - text: '', - }, - } as BetaRawMessageStreamEvent - } - - if (part.text) { - yield { - type: 'content_block_delta', - index: openTextLikeBlock.index, - delta: - textLikeType === 'thinking' - ? { - type: 'thinking_delta', - thinking: part.text, - } - : { - type: 'text_delta', - text: part.text, - }, - } as BetaRawMessageStreamEvent - } - - if (part.thoughtSignature) { - yield { - type: 'content_block_delta', - index: openTextLikeBlock.index, - delta: { - type: 'signature_delta', - signature: part.thoughtSignature, - }, - } as BetaRawMessageStreamEvent - } - - continue - } - - if (part.thoughtSignature && openTextLikeBlock) { - yield { - type: 'content_block_delta', - index: openTextLikeBlock.index, - delta: { - type: 'signature_delta', - signature: part.thoughtSignature, - }, - } as BetaRawMessageStreamEvent - } - } - - if (candidate?.finishReason) { - finishReason = candidate.finishReason - } - } - - if (!started) { - return - } - - if (openTextLikeBlock) { - yield { - type: 'content_block_stop', - index: openTextLikeBlock.index, - } as BetaRawMessageStreamEvent - } - - if (!stopped) { - yield { - type: 'message_delta', - delta: { - stop_reason: mapGeminiFinishReason(finishReason, sawToolUse), - stop_sequence: null, - }, - usage: { - output_tokens: outputTokens, - }, - } as BetaRawMessageStreamEvent - - yield { - type: 'message_stop', - } as BetaRawMessageStreamEvent - stopped = true - } -} - -function getTextLikeBlockType( - part: GeminiPart, -): 'text' | 'thinking' | null { - if (typeof part.text !== 'string') { - return null - } - return part.thought ? 'thinking' : 'text' -} - -function mapGeminiFinishReason( - reason: string | undefined, - sawToolUse: boolean, -): string { - switch (reason) { - case 'MAX_TOKENS': - return 'max_tokens' - case 'STOP': - case 'FINISH_REASON_UNSPECIFIED': - case 'SAFETY': - case 'RECITATION': - case 'BLOCKLIST': - case 'PROHIBITED_CONTENT': - case 'SPII': - case 'MALFORMED_FUNCTION_CALL': - default: - return sawToolUse ? 'tool_use' : 'end_turn' - } -} +// Re-export from @anthropic-ai/model-provider +export { adaptGeminiStreamToAnthropic } from '@anthropic-ai/model-provider' diff --git a/src/services/api/gemini/types.ts b/src/services/api/gemini/types.ts index e8718fecd..bac430014 100644 --- a/src/services/api/gemini/types.ts +++ b/src/services/api/gemini/types.ts @@ -1,86 +1,16 @@ -export const GEMINI_THOUGHT_SIGNATURE_FIELD = '_geminiThoughtSignature' - -export type GeminiFunctionCall = { - name?: string - args?: Record -} - -export type GeminiFunctionResponse = { - name?: string - response?: Record -} - -export type GeminiInlineData = { - mimeType: string - data: string -} - -export type GeminiPart = { - text?: string - thought?: boolean - thoughtSignature?: string - functionCall?: GeminiFunctionCall - functionResponse?: GeminiFunctionResponse - inlineData?: GeminiInlineData -} - -export type GeminiContent = { - role: 'user' | 'model' - parts: GeminiPart[] -} - -export type GeminiFunctionDeclaration = { - name: string - description?: string - parameters?: Record - parametersJsonSchema?: Record -} - -export type GeminiTool = { - functionDeclarations: GeminiFunctionDeclaration[] -} - -export type GeminiFunctionCallingConfig = { - mode: 'AUTO' | 'ANY' | 'NONE' - allowedFunctionNames?: string[] -} - -export type GeminiGenerateContentRequest = { - contents: GeminiContent[] - systemInstruction?: { - parts: Array<{ text: string }> - } - tools?: GeminiTool[] - toolConfig?: { - functionCallingConfig: GeminiFunctionCallingConfig - } - generationConfig?: { - temperature?: number - thinkingConfig?: { - includeThoughts?: boolean - thinkingBudget?: number - } - } -} - -export type GeminiUsageMetadata = { - promptTokenCount?: number - candidatesTokenCount?: number - thoughtsTokenCount?: number - totalTokenCount?: number -} - -export type GeminiCandidate = { - content?: { - role?: string - parts?: GeminiPart[] - } - finishReason?: string - index?: number -} - -export type GeminiStreamChunk = { - candidates?: GeminiCandidate[] - usageMetadata?: GeminiUsageMetadata - modelVersion?: string -} +// Re-export from @anthropic-ai/model-provider +export { + GEMINI_THOUGHT_SIGNATURE_FIELD, + type GeminiContent, + type GeminiGenerateContentRequest, + type GeminiPart, + type GeminiStreamChunk, + type GeminiTool, + type GeminiFunctionCallingConfig, + type GeminiFunctionDeclaration, + type GeminiFunctionCall, + type GeminiFunctionResponse, + type GeminiInlineData, + type GeminiUsageMetadata, + type GeminiCandidate, +} from '@anthropic-ai/model-provider'