| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- import { Anthropic } from "@anthropic-ai/sdk"
- import {
- Content,
- EnhancedGenerateContentResponse,
- FunctionCallPart,
- FunctionDeclaration,
- FunctionResponsePart,
- InlineDataPart,
- Part,
- SchemaType,
- TextPart,
- } from "@google/generative-ai"
- export function convertAnthropicContentToGemini(
- content:
- | string
- | Array<
- | Anthropic.Messages.TextBlockParam
- | Anthropic.Messages.ImageBlockParam
- | Anthropic.Messages.ToolUseBlockParam
- | Anthropic.Messages.ToolResultBlockParam
- >,
- ): Part[] {
- if (typeof content === "string") {
- return [{ text: content } as TextPart]
- }
- return content.flatMap((block) => {
- switch (block.type) {
- case "text":
- return { text: block.text } as TextPart
- case "image":
- if (block.source.type !== "base64") {
- throw new Error("Unsupported image source type")
- }
- return {
- inlineData: {
- data: block.source.data,
- mimeType: block.source.media_type,
- },
- } as InlineDataPart
- case "tool_use":
- return {
- functionCall: {
- name: block.name,
- args: block.input,
- },
- } as FunctionCallPart
- case "tool_result":
- const name = block.tool_use_id.split("-")[0]
- if (!block.content) {
- return []
- }
- if (typeof block.content === "string") {
- return {
- functionResponse: {
- name,
- response: {
- name,
- content: block.content,
- },
- },
- } as FunctionResponsePart
- } else {
- // The only case when tool_result could be array is when the tool failed and we're providing ie user feedback potentially with images
- const textParts = block.content.filter((part) => part.type === "text")
- const imageParts = block.content.filter((part) => part.type === "image")
- const text = textParts.length > 0 ? textParts.map((part) => part.text).join("\n\n") : ""
- const imageText = imageParts.length > 0 ? "\n\n(See next part for image)" : ""
- return [
- {
- functionResponse: {
- name,
- response: {
- name,
- content: text + imageText,
- },
- },
- } as FunctionResponsePart,
- ...imageParts.map(
- (part) =>
- ({
- inlineData: {
- data: part.source.data,
- mimeType: part.source.media_type,
- },
- }) as InlineDataPart,
- ),
- ]
- }
- default:
- throw new Error(`Unsupported content block type: ${(block as any).type}`)
- }
- })
- }
- export function convertAnthropicMessageToGemini(message: Anthropic.Messages.MessageParam): Content {
- return {
- role: message.role === "assistant" ? "model" : "user",
- parts: convertAnthropicContentToGemini(message.content),
- }
- }
- export function convertAnthropicToolToGemini(tool: Anthropic.Messages.Tool): FunctionDeclaration {
- return {
- name: tool.name,
- description: tool.description || "",
- parameters: {
- type: SchemaType.OBJECT,
- properties: Object.fromEntries(
- Object.entries(tool.input_schema.properties || {}).map(([key, value]) => [
- key,
- {
- type: (value as any).type.toUpperCase(),
- description: (value as any).description || "",
- },
- ]),
- ),
- required: (tool.input_schema.required as string[]) || [],
- },
- }
- }
- /*
- It looks like gemini likes to double escape certain characters when writing file contents: https://discuss.ai.google.dev/t/function-call-string-property-is-double-escaped/37867
- */
- export function unescapeGeminiContent(content: string) {
- return content
- .replace(/\\n/g, "\n")
- .replace(/\\'/g, "'")
- .replace(/\\"/g, '"')
- .replace(/\\r/g, "\r")
- .replace(/\\t/g, "\t")
- }
- export function convertGeminiResponseToAnthropic(
- response: EnhancedGenerateContentResponse,
- ): Anthropic.Messages.Message {
- const content: Anthropic.Messages.ContentBlock[] = []
- // Add the main text response
- const text = response.text()
- if (text) {
- content.push({ type: "text", text })
- }
- // Add function calls as tool_use blocks
- const functionCalls = response.functionCalls()
- if (functionCalls) {
- functionCalls.forEach((call, index) => {
- if ("content" in call.args && typeof call.args.content === "string") {
- call.args.content = unescapeGeminiContent(call.args.content)
- }
- content.push({
- type: "tool_use",
- id: `${call.name}-${index}-${Date.now()}`,
- name: call.name,
- input: call.args,
- })
- })
- }
- // Determine stop reason
- let stop_reason: Anthropic.Messages.Message["stop_reason"] = null
- const finishReason = response.candidates?.[0]?.finishReason
- if (finishReason) {
- switch (finishReason) {
- case "STOP":
- stop_reason = "end_turn"
- break
- case "MAX_TOKENS":
- stop_reason = "max_tokens"
- break
- case "SAFETY":
- case "RECITATION":
- case "OTHER":
- stop_reason = "stop_sequence"
- break
- // Add more cases if needed
- }
- }
- return {
- id: `msg_${Date.now()}`, // Generate a unique ID
- type: "message",
- role: "assistant",
- content,
- model: "",
- stop_reason,
- stop_sequence: null, // Gemini doesn't provide this information
- usage: {
- input_tokens: response.usageMetadata?.promptTokenCount ?? 0,
- output_tokens: response.usageMetadata?.candidatesTokenCount ?? 0,
- },
- }
- }
|