mirror of
https://github.com/cline/cline.git
synced 2026-09-19 02:05:44 +08:00
fix(gemini): preserve tool response names
This commit is contained in:
@@ -6,7 +6,7 @@ import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { sanitizeAnthropicMessages } from "../transform/anthropic-format"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
import { convertAnthropicMessagesToGemini } from "../transform/gemini-format"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
@@ -285,7 +285,7 @@ export class AIhubmixHandler implements ApiHandler {
|
||||
const client = this.ensureGeminiClient()
|
||||
const modelId = this.options.modelId || "gemini-2.0-flash-exp"
|
||||
|
||||
const contents = messages.map(convertAnthropicMessageToGemini)
|
||||
const contents = convertAnthropicMessagesToGemini(messages)
|
||||
|
||||
const requestConfig: GenerateContentConfig = {
|
||||
systemInstruction: systemPrompt,
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { RetriableError, withRetry } from "../retry"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
import { convertAnthropicMessagesToGemini } from "../transform/gemini-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
const rateLimitPatterns = [/got status: 429/i, /429 Too Many Requests/i, /rate limit exceeded/i, /too many requests/i]
|
||||
@@ -148,7 +148,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: GoogleTool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const { id: modelId, info } = this.getModel()
|
||||
const contents = messages.map(convertAnthropicMessageToGemini)
|
||||
const contents = convertAnthropicMessagesToGemini(messages)
|
||||
// Gemini may emit multiple function calls under the same responseId and without functionCall.id.
|
||||
// Track a local sequence so each emitted tool call has a stable unique ID.
|
||||
const responseToolCallCount = new Map<string, number>()
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import "should"
|
||||
import { describe, it } from "mocha"
|
||||
import { convertAnthropicMessagesToGemini } from "../gemini-format"
|
||||
|
||||
describe("gemini-format", () => {
|
||||
it("uses the original tool name for function responses and preserves the tool call id", () => {
|
||||
const contents = convertAnthropicMessagesToGemini([
|
||||
{
|
||||
role: "user",
|
||||
content: "list files",
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "resp_1-tool-0",
|
||||
name: "run_commands",
|
||||
input: { commands: ["ls -la /app"] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "resp_1-tool-0",
|
||||
content: "total 0",
|
||||
},
|
||||
],
|
||||
},
|
||||
] as any)
|
||||
|
||||
const functionCall = contents[1].parts![0].functionCall!
|
||||
const functionResponse = contents[2].parts![0].functionResponse!
|
||||
|
||||
functionCall.should.deepEqual({
|
||||
id: "resp_1-tool-0",
|
||||
name: "run_commands",
|
||||
args: { commands: ["ls -la /app"] },
|
||||
})
|
||||
functionResponse.should.deepEqual({
|
||||
id: "resp_1-tool-0",
|
||||
name: "run_commands",
|
||||
response: {
|
||||
result: "total 0",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("can resolve function response names from Cline call_id metadata", () => {
|
||||
const contents = convertAnthropicMessagesToGemini([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "tool-use-id",
|
||||
call_id: "provider-call-id",
|
||||
name: "read_file",
|
||||
input: { path: "/tmp/example.txt" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool-use-id",
|
||||
call_id: "provider-call-id",
|
||||
content: "hello",
|
||||
},
|
||||
],
|
||||
},
|
||||
] as any)
|
||||
|
||||
const functionResponse = contents[1].parts![0].functionResponse!
|
||||
functionResponse.name.should.equal("read_file")
|
||||
functionResponse.id.should.equal("tool-use-id")
|
||||
})
|
||||
})
|
||||
@@ -10,7 +10,32 @@ import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
// "context_engineering_is_the_way_to_go" or "skip_thought_signature_validator" in the thought signature field to skip validation.
|
||||
const GEMINI_DUMMY_THOUGHT_SIGNATURE = "skip_thought_signature_validator"
|
||||
|
||||
export function convertAnthropicContentToGemini(content: string | ClineStorageMessage["content"]): Part[] {
|
||||
type GeminiToolNameById = Map<string, string>
|
||||
|
||||
function rememberGeminiToolUse(toolNameById: GeminiToolNameById | undefined, block: Anthropic.ToolUseBlockParam) {
|
||||
if (!toolNameById) {
|
||||
return
|
||||
}
|
||||
|
||||
if (block.id) {
|
||||
toolNameById.set(block.id, block.name)
|
||||
}
|
||||
|
||||
const callId = (block as { call_id?: string }).call_id
|
||||
if (callId) {
|
||||
toolNameById.set(callId, block.name)
|
||||
}
|
||||
}
|
||||
|
||||
function getGeminiFunctionResponseName(block: Anthropic.ToolResultBlockParam, toolNameById: GeminiToolNameById | undefined) {
|
||||
const callId = (block as { call_id?: string }).call_id
|
||||
return toolNameById?.get(block.tool_use_id) ?? (callId ? toolNameById?.get(callId) : undefined) ?? block.tool_use_id
|
||||
}
|
||||
|
||||
export function convertAnthropicContentToGemini(
|
||||
content: string | ClineStorageMessage["content"],
|
||||
toolNameById?: GeminiToolNameById,
|
||||
): Part[] {
|
||||
if (typeof content === "string") {
|
||||
return [{ text: content }]
|
||||
}
|
||||
@@ -30,8 +55,10 @@ export function convertAnthropicContentToGemini(content: string | ClineStorageMe
|
||||
},
|
||||
}
|
||||
case "tool_use":
|
||||
rememberGeminiToolUse(toolNameById, block)
|
||||
return {
|
||||
functionCall: {
|
||||
id: block.id,
|
||||
name: block.name,
|
||||
args: block.input as Record<string, unknown>,
|
||||
},
|
||||
@@ -39,9 +66,14 @@ export function convertAnthropicContentToGemini(content: string | ClineStorageMe
|
||||
thoughtSignature: block.signature || GEMINI_DUMMY_THOUGHT_SIGNATURE,
|
||||
}
|
||||
case "tool_result":
|
||||
const name = getGeminiFunctionResponseName(block, toolNameById)
|
||||
if (!name) {
|
||||
throw new Error("Cannot convert Gemini tool result without a matching function name")
|
||||
}
|
||||
return {
|
||||
functionResponse: {
|
||||
name: block.tool_use_id,
|
||||
id: block.tool_use_id,
|
||||
name,
|
||||
response: {
|
||||
result: block.content,
|
||||
},
|
||||
@@ -60,13 +92,21 @@ export function convertAnthropicContentToGemini(content: string | ClineStorageMe
|
||||
.filter((part): part is Part => part !== undefined) // Filter out unsupported blocks
|
||||
}
|
||||
|
||||
export function convertAnthropicMessageToGemini(message: Anthropic.Messages.MessageParam): Content {
|
||||
export function convertAnthropicMessageToGemini(
|
||||
message: Anthropic.Messages.MessageParam,
|
||||
toolNameById?: GeminiToolNameById,
|
||||
): Content {
|
||||
return {
|
||||
role: message.role === "assistant" ? "model" : "user",
|
||||
parts: convertAnthropicContentToGemini(message.content),
|
||||
parts: convertAnthropicContentToGemini(message.content, toolNameById),
|
||||
}
|
||||
}
|
||||
|
||||
export function convertAnthropicMessagesToGemini(messages: Anthropic.Messages.MessageParam[]): Content[] {
|
||||
const toolNameById: GeminiToolNameById = new Map()
|
||||
return messages.map((message) => convertAnthropicMessageToGemini(message, toolNameById))
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user