gemini-format.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import { Anthropic } from "@anthropic-ai/sdk"
  2. import {
  3. Content,
  4. EnhancedGenerateContentResponse,
  5. FunctionCallPart,
  6. FunctionDeclaration,
  7. FunctionResponsePart,
  8. InlineDataPart,
  9. Part,
  10. SchemaType,
  11. TextPart,
  12. } from "@google/generative-ai"
  13. export function convertAnthropicContentToGemini(
  14. content:
  15. | string
  16. | Array<
  17. | Anthropic.Messages.TextBlockParam
  18. | Anthropic.Messages.ImageBlockParam
  19. | Anthropic.Messages.ToolUseBlockParam
  20. | Anthropic.Messages.ToolResultBlockParam
  21. >,
  22. ): Part[] {
  23. if (typeof content === "string") {
  24. return [{ text: content } as TextPart]
  25. }
  26. return content.flatMap((block) => {
  27. switch (block.type) {
  28. case "text":
  29. return { text: block.text } as TextPart
  30. case "image":
  31. if (block.source.type !== "base64") {
  32. throw new Error("Unsupported image source type")
  33. }
  34. return {
  35. inlineData: {
  36. data: block.source.data,
  37. mimeType: block.source.media_type,
  38. },
  39. } as InlineDataPart
  40. case "tool_use":
  41. return {
  42. functionCall: {
  43. name: block.name,
  44. args: block.input,
  45. },
  46. } as FunctionCallPart
  47. case "tool_result":
  48. const name = block.tool_use_id.split("-")[0]
  49. if (!block.content) {
  50. return []
  51. }
  52. if (typeof block.content === "string") {
  53. return {
  54. functionResponse: {
  55. name,
  56. response: {
  57. name,
  58. content: block.content,
  59. },
  60. },
  61. } as FunctionResponsePart
  62. } else {
  63. // The only case when tool_result could be array is when the tool failed and we're providing ie user feedback potentially with images
  64. const textParts = block.content.filter((part) => part.type === "text")
  65. const imageParts = block.content.filter((part) => part.type === "image")
  66. const text = textParts.length > 0 ? textParts.map((part) => part.text).join("\n\n") : ""
  67. const imageText = imageParts.length > 0 ? "\n\n(See next part for image)" : ""
  68. return [
  69. {
  70. functionResponse: {
  71. name,
  72. response: {
  73. name,
  74. content: text + imageText,
  75. },
  76. },
  77. } as FunctionResponsePart,
  78. ...imageParts.map(
  79. (part) =>
  80. ({
  81. inlineData: {
  82. data: part.source.data,
  83. mimeType: part.source.media_type,
  84. },
  85. }) as InlineDataPart,
  86. ),
  87. ]
  88. }
  89. default:
  90. throw new Error(`Unsupported content block type: ${(block as any).type}`)
  91. }
  92. })
  93. }
  94. export function convertAnthropicMessageToGemini(message: Anthropic.Messages.MessageParam): Content {
  95. return {
  96. role: message.role === "assistant" ? "model" : "user",
  97. parts: convertAnthropicContentToGemini(message.content),
  98. }
  99. }
  100. export function convertAnthropicToolToGemini(tool: Anthropic.Messages.Tool): FunctionDeclaration {
  101. return {
  102. name: tool.name,
  103. description: tool.description || "",
  104. parameters: {
  105. type: SchemaType.OBJECT,
  106. properties: Object.fromEntries(
  107. Object.entries(tool.input_schema.properties || {}).map(([key, value]) => [
  108. key,
  109. {
  110. type: (value as any).type.toUpperCase(),
  111. description: (value as any).description || "",
  112. },
  113. ]),
  114. ),
  115. required: (tool.input_schema.required as string[]) || [],
  116. },
  117. }
  118. }
  119. /*
  120. 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
  121. */
  122. export function unescapeGeminiContent(content: string) {
  123. return content
  124. .replace(/\\n/g, "\n")
  125. .replace(/\\'/g, "'")
  126. .replace(/\\"/g, '"')
  127. .replace(/\\r/g, "\r")
  128. .replace(/\\t/g, "\t")
  129. }
  130. export function convertGeminiResponseToAnthropic(
  131. response: EnhancedGenerateContentResponse,
  132. ): Anthropic.Messages.Message {
  133. const content: Anthropic.Messages.ContentBlock[] = []
  134. // Add the main text response
  135. const text = response.text()
  136. if (text) {
  137. content.push({ type: "text", text })
  138. }
  139. // Add function calls as tool_use blocks
  140. const functionCalls = response.functionCalls()
  141. if (functionCalls) {
  142. functionCalls.forEach((call, index) => {
  143. if ("content" in call.args && typeof call.args.content === "string") {
  144. call.args.content = unescapeGeminiContent(call.args.content)
  145. }
  146. content.push({
  147. type: "tool_use",
  148. id: `${call.name}-${index}-${Date.now()}`,
  149. name: call.name,
  150. input: call.args,
  151. })
  152. })
  153. }
  154. // Determine stop reason
  155. let stop_reason: Anthropic.Messages.Message["stop_reason"] = null
  156. const finishReason = response.candidates?.[0]?.finishReason
  157. if (finishReason) {
  158. switch (finishReason) {
  159. case "STOP":
  160. stop_reason = "end_turn"
  161. break
  162. case "MAX_TOKENS":
  163. stop_reason = "max_tokens"
  164. break
  165. case "SAFETY":
  166. case "RECITATION":
  167. case "OTHER":
  168. stop_reason = "stop_sequence"
  169. break
  170. // Add more cases if needed
  171. }
  172. }
  173. return {
  174. id: `msg_${Date.now()}`, // Generate a unique ID
  175. type: "message",
  176. role: "assistant",
  177. content,
  178. model: "",
  179. stop_reason,
  180. stop_sequence: null, // Gemini doesn't provide this information
  181. usage: {
  182. input_tokens: response.usageMetadata?.promptTokenCount ?? 0,
  183. output_tokens: response.usageMetadata?.candidatesTokenCount ?? 0,
  184. },
  185. }
  186. }