feat: 添加gemini协议适配 (#125)

* feat: 添加gemini协议适配

* Remove unrelated local files from Gemini PR
This commit is contained in:
SaltedFish555
2026-04-06 09:55:20 +08:00
committed by GitHub
parent 27825293bb
commit 0da5ec09e8
24 changed files with 2257 additions and 38 deletions

View File

@@ -0,0 +1,278 @@
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<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, 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),
},
},
]
}
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),
),
)
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) && {
thoughtSignature: getGeminiThoughtSignature(block),
}),
})
}
}
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, is_error: true } : result
}
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
}