|
|
@@ -1,5 +1,15 @@
|
|
|
import { Anthropic } from "@anthropic-ai/sdk"
|
|
|
-import { Content, EnhancedGenerateContentResponse, FunctionDeclaration, Part, SchemaType } from "@google/generative-ai"
|
|
|
+import {
|
|
|
+ Content,
|
|
|
+ EnhancedGenerateContentResponse,
|
|
|
+ FunctionCallPart,
|
|
|
+ FunctionDeclaration,
|
|
|
+ FunctionResponsePart,
|
|
|
+ InlineDataPart,
|
|
|
+ Part,
|
|
|
+ SchemaType,
|
|
|
+ TextPart,
|
|
|
+} from "@google/generative-ai"
|
|
|
|
|
|
export function convertAnthropicContentToGemini(
|
|
|
content:
|
|
|
@@ -12,12 +22,12 @@ export function convertAnthropicContentToGemini(
|
|
|
>
|
|
|
): Part[] {
|
|
|
if (typeof content === "string") {
|
|
|
- return [{ text: content }]
|
|
|
+ return [{ text: content } as TextPart]
|
|
|
}
|
|
|
- return content.map((block) => {
|
|
|
+ return content.flatMap((block) => {
|
|
|
switch (block.type) {
|
|
|
case "text":
|
|
|
- return { text: block.text }
|
|
|
+ return { text: block.text } as TextPart
|
|
|
case "image":
|
|
|
if (block.source.type !== "base64") {
|
|
|
throw new Error("Unsupported image source type")
|
|
|
@@ -27,22 +37,55 @@ export function convertAnthropicContentToGemini(
|
|
|
data: block.source.data,
|
|
|
mimeType: block.source.media_type,
|
|
|
},
|
|
|
- }
|
|
|
+ } as InlineDataPart
|
|
|
case "tool_use":
|
|
|
return {
|
|
|
functionCall: {
|
|
|
name: block.name,
|
|
|
args: block.input,
|
|
|
},
|
|
|
- } as Part
|
|
|
+ } as FunctionCallPart
|
|
|
case "tool_result":
|
|
|
- return {
|
|
|
- functionResponse: {
|
|
|
- name: block.tool_use_id,
|
|
|
- response: {
|
|
|
- content: block.content,
|
|
|
+ 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}`)
|
|
|
@@ -52,7 +95,7 @@ export function convertAnthropicContentToGemini(
|
|
|
|
|
|
export function convertAnthropicMessageToGemini(message: Anthropic.Messages.MessageParam): Content {
|
|
|
return {
|
|
|
- role: message.role === "assistant" ? "model" : message.role,
|
|
|
+ role: message.role === "assistant" ? "model" : "user",
|
|
|
parts: convertAnthropicContentToGemini(message.content),
|
|
|
}
|
|
|
}
|
|
|
@@ -77,6 +120,13 @@ export function convertAnthropicToolToGemini(tool: Anthropic.Messages.Tool): Fun
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/*
|
|
|
+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, '"')
|
|
|
+}
|
|
|
+
|
|
|
export function convertGeminiResponseToAnthropic(
|
|
|
response: EnhancedGenerateContentResponse
|
|
|
): Anthropic.Messages.Message {
|
|
|
@@ -92,9 +142,12 @@ export function convertGeminiResponseToAnthropic(
|
|
|
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: `tool_${index}`,
|
|
|
+ id: `${call.name}-${index}-${Date.now()}`,
|
|
|
name: call.name,
|
|
|
input: call.args,
|
|
|
})
|