mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 16:25:51 +00:00
refactor: 搬入 Gemini 兼容层到 model-provider 包
- 搬入 Gemini 类型定义、消息转换、工具转换、流适配、模型映射 - 主项目 gemini/ 目录下文件改为 thin re-export proxy Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<GeminiGenerateContentRequest, 'contents' | 'systemInstruction'> {
|
||||||
|
const contents: GeminiContent[] = []
|
||||||
|
const toolNamesById = new Map<string, string>()
|
||||||
|
|
||||||
|
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<string, string>,
|
||||||
|
): 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<string, unknown>, toolNamesById),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertUserContentBlockToGeminiParts(
|
||||||
|
block: string | Record<string, unknown>,
|
||||||
|
toolNamesById: ReadonlyMap<string, string>,
|
||||||
|
): 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<string, unknown> | 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<string, unknown>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
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<string, unknown>) && {
|
||||||
|
thoughtSignature: getGeminiThoughtSignature(block as unknown as Record<string, unknown>),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string, unknown> {
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
const parsed = safeParseJSON(input)
|
||||||
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
|
return parsed as Record<string, unknown>
|
||||||
|
}
|
||||||
|
return parsed === null ? {} : { value: parsed }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input && typeof input === 'object' && !Array.isArray(input)) {
|
||||||
|
return input as Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
return input === undefined ? {} : { value: input }
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolResultToResponseObject(
|
||||||
|
block: BetaToolResultBlockParam,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const result = normalizeToolResultContent(block.content)
|
||||||
|
if (
|
||||||
|
result &&
|
||||||
|
typeof result === 'object' &&
|
||||||
|
!Array.isArray(result)
|
||||||
|
) {
|
||||||
|
return block.is_error ? { ...(result as Record<string, unknown>), is_error: true } : result as Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
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, unknown>): string | undefined {
|
||||||
|
const signature = block[GEMINI_THOUGHT_SIGNATURE_FIELD]
|
||||||
|
return typeof signature === 'string' && signature.length > 0
|
||||||
|
? signature
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
@@ -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<string, Record<string, unknown>> | undefined {
|
||||||
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedEntries = Object.entries(value as Record<string, unknown>)
|
||||||
|
.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<string, unknown>[] | 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<string, unknown> {
|
||||||
|
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = schema as Record<string, unknown>
|
||||||
|
const result: Record<string, unknown> = {}
|
||||||
|
|
||||||
|
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<string, unknown> {
|
||||||
|
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<string, unknown>
|
||||||
|
const name = (anyTool.name as string) || ''
|
||||||
|
const description = (anyTool.description as string) || ''
|
||||||
|
const inputSchema =
|
||||||
|
(anyTool.input_schema as Record<string, unknown> | 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<string, unknown>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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<GeminiStreamChunk>,
|
||||||
|
model: string,
|
||||||
|
): AsyncGenerator<BetaRawMessageStreamEvent, void> {
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
export const GEMINI_THOUGHT_SIGNATURE_FIELD = '_geminiThoughtSignature'
|
||||||
|
|
||||||
|
export type GeminiFunctionCall = {
|
||||||
|
name?: string
|
||||||
|
args?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GeminiFunctionResponse = {
|
||||||
|
name?: string
|
||||||
|
response?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string, unknown>
|
||||||
|
parametersJsonSchema?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,298 +1,2 @@
|
|||||||
import type {
|
// Re-export from @anthropic-ai/model-provider
|
||||||
BetaToolResultBlockParam,
|
export { anthropicMessagesToGemini } from '@anthropic-ai/model-provider'
|
||||||
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<GeminiGenerateContentRequest, 'contents' | 'systemInstruction'> {
|
|
||||||
const contents: GeminiContent[] = []
|
|
||||||
const toolNamesById = new Map<string, string>()
|
|
||||||
|
|
||||||
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<string, string>,
|
|
||||||
): 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<string, unknown>, toolNamesById),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertUserContentBlockToGeminiParts(
|
|
||||||
block: string | Record<string, unknown>,
|
|
||||||
toolNamesById: ReadonlyMap<string, string>,
|
|
||||||
): 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<string, unknown> | 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<string, unknown>),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
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<string, unknown>) && {
|
|
||||||
thoughtSignature: getGeminiThoughtSignature(block as unknown as Record<string, unknown>),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<string, unknown> {
|
|
||||||
if (typeof input === 'string') {
|
|
||||||
const parsed = safeParseJSON(input)
|
|
||||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
||||||
return parsed as Record<string, unknown>
|
|
||||||
}
|
|
||||||
return parsed === null ? {} : { value: parsed }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input && typeof input === 'object' && !Array.isArray(input)) {
|
|
||||||
return input as Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
return input === undefined ? {} : { value: input }
|
|
||||||
}
|
|
||||||
|
|
||||||
function toolResultToResponseObject(
|
|
||||||
block: BetaToolResultBlockParam,
|
|
||||||
): Record<string, unknown> {
|
|
||||||
const result = normalizeToolResultContent(block.content)
|
|
||||||
if (
|
|
||||||
result &&
|
|
||||||
typeof result === 'object' &&
|
|
||||||
!Array.isArray(result)
|
|
||||||
) {
|
|
||||||
return block.is_error ? { ...(result as Record<string, unknown>), is_error: true } : result as Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
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, unknown>): string | undefined {
|
|
||||||
const signature = block[GEMINI_THOUGHT_SIGNATURE_FIELD]
|
|
||||||
return typeof signature === 'string' && signature.length > 0
|
|
||||||
? signature
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,285 +1,2 @@
|
|||||||
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
// Re-export from @anthropic-ai/model-provider
|
||||||
import type {
|
export { anthropicToolsToGemini, anthropicToolChoiceToGemini } from '@anthropic-ai/model-provider'
|
||||||
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<string, Record<string, unknown>> | undefined {
|
|
||||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const sanitizedEntries = Object.entries(value as Record<string, unknown>)
|
|
||||||
.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<string, unknown>[] | 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<string, unknown> {
|
|
||||||
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = schema as Record<string, unknown>
|
|
||||||
const result: Record<string, unknown> = {}
|
|
||||||
|
|
||||||
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<string, unknown> {
|
|
||||||
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<string, unknown>
|
|
||||||
const name = (anyTool.name as string) || ''
|
|
||||||
const description = (anyTool.description as string) || ''
|
|
||||||
const inputSchema =
|
|
||||||
(anyTool.input_schema as Record<string, unknown> | 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<string, unknown>
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,37 +1,2 @@
|
|||||||
function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null {
|
// Re-export from @anthropic-ai/model-provider
|
||||||
if (/haiku/i.test(model)) return 'haiku'
|
export { resolveGeminiModel } from '@anthropic-ai/model-provider'
|
||||||
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.`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,243 +1,2 @@
|
|||||||
import type { BetaRawMessageStreamEvent } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
// Re-export from @anthropic-ai/model-provider
|
||||||
import { randomUUID } from 'crypto'
|
export { adaptGeminiStreamToAnthropic } from '@anthropic-ai/model-provider'
|
||||||
import type { GeminiPart, GeminiStreamChunk } from './types.js'
|
|
||||||
|
|
||||||
export async function* adaptGeminiStreamToAnthropic(
|
|
||||||
stream: AsyncIterable<GeminiStreamChunk>,
|
|
||||||
model: string,
|
|
||||||
): AsyncGenerator<BetaRawMessageStreamEvent, void> {
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,86 +1,16 @@
|
|||||||
export const GEMINI_THOUGHT_SIGNATURE_FIELD = '_geminiThoughtSignature'
|
// Re-export from @anthropic-ai/model-provider
|
||||||
|
export {
|
||||||
export type GeminiFunctionCall = {
|
GEMINI_THOUGHT_SIGNATURE_FIELD,
|
||||||
name?: string
|
type GeminiContent,
|
||||||
args?: Record<string, unknown>
|
type GeminiGenerateContentRequest,
|
||||||
}
|
type GeminiPart,
|
||||||
|
type GeminiStreamChunk,
|
||||||
export type GeminiFunctionResponse = {
|
type GeminiTool,
|
||||||
name?: string
|
type GeminiFunctionCallingConfig,
|
||||||
response?: Record<string, unknown>
|
type GeminiFunctionDeclaration,
|
||||||
}
|
type GeminiFunctionCall,
|
||||||
|
type GeminiFunctionResponse,
|
||||||
export type GeminiInlineData = {
|
type GeminiInlineData,
|
||||||
mimeType: string
|
type GeminiUsageMetadata,
|
||||||
data: string
|
type GeminiCandidate,
|
||||||
}
|
} from '@anthropic-ai/model-provider'
|
||||||
|
|
||||||
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<string, unknown>
|
|
||||||
parametersJsonSchema?: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user