mirror of
https://github.com/cline/cline.git
synced 2026-09-17 06:18:11 +08:00
* log + options * changeset --------- Co-authored-by: Cline Evaluation <cline@example.com>
344 lines
9.6 KiB
TypeScript
344 lines
9.6 KiB
TypeScript
import { OpenRouterHandler } from "../../src/api/providers/openrouter"
|
|
import { ApiHandlerOptions } from "../../src/shared/api"
|
|
import { Anthropic } from "@anthropic-ai/sdk"
|
|
|
|
import {
|
|
parseAssistantMessageV1,
|
|
parseAssistantMessageV2,
|
|
parseAssistantMessageV3,
|
|
AssistantMessageContent,
|
|
} from "./parsing/parse-assistant-message-06-06-25" // "../../src/core/assistant-message"
|
|
import { constructNewFileContent as constructNewFileContent_06_06_25 } from "./diff-apply/diff-06-06-25"
|
|
import { constructNewFileContent as constructNewFileContent_06_23_25 } from "./diff-apply/diff-06-23-25"
|
|
import { constructNewFileContent as constructNewFileContent_06_25_25 } from "./diff-apply/diff-06-25-25"
|
|
import { constructNewFileContent as constructNewFileContent_06_26_25 } from "./diff-apply/diff-06-26-25"
|
|
|
|
type ParseAssistantMessageFn = (message: string) => AssistantMessageContent[]
|
|
type ConstructNewFileContentFn = (diff: string, original: string, strict: boolean) => Promise<string | any>
|
|
|
|
const parsingFunctions: Record<string, ParseAssistantMessageFn> = {
|
|
parseAssistantMessageV1: parseAssistantMessageV1,
|
|
parseAssistantMessageV2: parseAssistantMessageV2,
|
|
parseAssistantMessageV3: parseAssistantMessageV3,
|
|
}
|
|
|
|
const diffEditingFunctions: Record<string, ConstructNewFileContentFn> = {
|
|
"diff-06-06-25": constructNewFileContent_06_06_25,
|
|
"diff-06-23-25": constructNewFileContent_06_23_25,
|
|
"diff-06-25-25": constructNewFileContent_06_25_25,
|
|
"diff-06-26-25": constructNewFileContent_06_26_25,
|
|
}
|
|
|
|
import { TestInput, TestResult, ExtractedToolCall } from "./types"
|
|
import { log } from "./helpers"
|
|
export { TestInput, TestResult, ExtractedToolCall }
|
|
|
|
interface StreamResult {
|
|
assistantMessage: string
|
|
reasoningMessage: string
|
|
usage: {
|
|
inputTokens: number
|
|
outputTokens: number
|
|
cacheWriteTokens: number
|
|
cacheReadTokens: number
|
|
totalCost: number
|
|
}
|
|
timing?: {
|
|
timeToFirstTokenMs: number
|
|
timeToFirstEditMs?: number
|
|
totalRoundTripMs: number
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process the stream and return full response with timing data
|
|
*/
|
|
async function processStream(
|
|
handler: OpenRouterHandler,
|
|
systemPrompt: string,
|
|
messages: Anthropic.Messages.MessageParam[],
|
|
): Promise<StreamResult> {
|
|
const startTime = Date.now()
|
|
const stream = handler.createMessage(systemPrompt, messages)
|
|
|
|
let assistantMessage = ""
|
|
let reasoningMessage = ""
|
|
let inputTokens = 0
|
|
let outputTokens = 0
|
|
let cacheWriteTokens = 0
|
|
let cacheReadTokens = 0
|
|
let totalCost = 0
|
|
|
|
// Timing tracking
|
|
let timeToFirstTokenMs: number | null = null
|
|
let timeToFirstEditMs: number | null = null
|
|
|
|
for await (const chunk of stream) {
|
|
if (!chunk) {
|
|
continue
|
|
}
|
|
|
|
// Capture time to first token (any chunk type)
|
|
if (timeToFirstTokenMs === null) {
|
|
timeToFirstTokenMs = Date.now() - startTime
|
|
}
|
|
|
|
switch (chunk.type) {
|
|
case "usage":
|
|
inputTokens += chunk.inputTokens
|
|
outputTokens += chunk.outputTokens
|
|
cacheWriteTokens += chunk.cacheWriteTokens ?? 0
|
|
cacheReadTokens += chunk.cacheReadTokens ?? 0
|
|
if (chunk.totalCost) {
|
|
totalCost = chunk.totalCost
|
|
}
|
|
break
|
|
case "reasoning":
|
|
reasoningMessage += chunk.reasoning
|
|
break
|
|
case "text":
|
|
assistantMessage += chunk.text
|
|
|
|
// Try to detect first tool call by parsing accumulated message
|
|
if (timeToFirstEditMs === null) {
|
|
try {
|
|
const parsed = parseAssistantMessageV2(assistantMessage)
|
|
const hasToolCall = parsed.some(block => block.type === "tool_use")
|
|
if (hasToolCall) {
|
|
timeToFirstEditMs = Date.now() - startTime
|
|
}
|
|
} catch {
|
|
// Parsing failed, continue accumulating
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
const totalRoundTripMs = Date.now() - startTime
|
|
|
|
return {
|
|
assistantMessage,
|
|
reasoningMessage,
|
|
usage: {
|
|
inputTokens,
|
|
outputTokens,
|
|
cacheWriteTokens,
|
|
cacheReadTokens,
|
|
totalCost,
|
|
},
|
|
timing: {
|
|
timeToFirstTokenMs: timeToFirstTokenMs || 0,
|
|
timeToFirstEditMs: timeToFirstEditMs || undefined,
|
|
totalRoundTripMs,
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main evaluation function:
|
|
* 1. create and process stream
|
|
* 2. extract any tool calls from the stream
|
|
* 3. if no diff edit, considered a failure (or rerun) - otherwise attempt to apply the diff edit
|
|
*/
|
|
export async function runSingleEvaluation(input: TestInput): Promise<TestResult> {
|
|
try {
|
|
// Extract parameters
|
|
const {
|
|
apiKey,
|
|
systemPrompt,
|
|
messages,
|
|
modelId,
|
|
originalFile,
|
|
originalFilePath,
|
|
parsingFunction,
|
|
diffEditFunction,
|
|
thinkingBudgetTokens,
|
|
originalDiffEditToolCallMessage,
|
|
diffApplyFile,
|
|
} = input
|
|
|
|
const requiredParams = {
|
|
systemPrompt,
|
|
messages,
|
|
modelId,
|
|
originalFile,
|
|
originalFilePath,
|
|
parsingFunction,
|
|
diffEditFunction,
|
|
}
|
|
|
|
const missingParams = Object.entries(requiredParams)
|
|
.filter(([, value]) => !value)
|
|
.map(([key]) => key)
|
|
|
|
if (missingParams.length > 0) {
|
|
return {
|
|
success: false,
|
|
error: "missing_required_parameters",
|
|
errorString: `Missing required parameters: ${missingParams.join(", ")}`,
|
|
}
|
|
}
|
|
|
|
const parseAssistantMessage = parsingFunctions[parsingFunction]
|
|
const constructNewFileContent = diffEditingFunctions[diffApplyFile || diffEditFunction]
|
|
|
|
if (!parseAssistantMessage || !constructNewFileContent) {
|
|
return {
|
|
success: false,
|
|
error: "invalid_functions",
|
|
}
|
|
}
|
|
|
|
const options: ApiHandlerOptions = {
|
|
openRouterApiKey: apiKey,
|
|
openRouterModelId: modelId,
|
|
thinkingBudgetTokens: thinkingBudgetTokens,
|
|
openRouterModelInfo: {
|
|
maxTokens: 10_000,
|
|
contextWindow: 1_000_000,
|
|
supportsImages: true,
|
|
supportsPromptCache: true, // may need to turn this on
|
|
inputPrice: 0,
|
|
outputPrice: 0,
|
|
},
|
|
}
|
|
|
|
// Get the output of streaming output of this llm call
|
|
let streamResult: StreamResult
|
|
if (originalDiffEditToolCallMessage !== undefined) {
|
|
// Replay mode: mock the stream result
|
|
streamResult = {
|
|
assistantMessage: originalDiffEditToolCallMessage,
|
|
reasoningMessage: "",
|
|
usage: { inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0, totalCost: 0 },
|
|
}
|
|
} else {
|
|
// Live mode: existing API call logic
|
|
try {
|
|
const openRouterHandler = new OpenRouterHandler(options)
|
|
streamResult = await processStream(openRouterHandler, systemPrompt, messages)
|
|
} catch (error: any) {
|
|
return {
|
|
success: false,
|
|
error: "llm_stream_error",
|
|
errorString: error.message || error.toString(),
|
|
}
|
|
}
|
|
}
|
|
|
|
// process the assistant message into its constituent tool calls & text blocks
|
|
const assistantContentBlocks: AssistantMessageContent[] = parseAssistantMessage(streamResult.assistantMessage)
|
|
|
|
const detectedToolCalls: ExtractedToolCall[] = []
|
|
|
|
for (const block of assistantContentBlocks) {
|
|
if (block.type === "tool_use") {
|
|
detectedToolCalls.push({
|
|
name: block.name,
|
|
input: block.params,
|
|
})
|
|
}
|
|
}
|
|
|
|
// check if there are any tool calls, if there are none then its a clear error
|
|
if (detectedToolCalls.length === 0) {
|
|
return {
|
|
success: false,
|
|
streamResult: streamResult,
|
|
toolCalls: detectedToolCalls,
|
|
error: "no_tool_calls",
|
|
}
|
|
}
|
|
|
|
// check that there is exactly one tool call, otherwise an error
|
|
if (detectedToolCalls.length > 1) {
|
|
return {
|
|
success: false,
|
|
streamResult: streamResult,
|
|
toolCalls: detectedToolCalls,
|
|
error: "multi_tool_calls",
|
|
}
|
|
}
|
|
|
|
// check that the tool call is diff edit tool call
|
|
if (detectedToolCalls[0].name !== "replace_in_file") {
|
|
return {
|
|
success: false,
|
|
streamResult: streamResult,
|
|
toolCalls: detectedToolCalls,
|
|
error: "wrong_tool_call",
|
|
}
|
|
}
|
|
|
|
const toolCall = detectedToolCalls[0]
|
|
const diffToolPath = toolCall.input.path
|
|
const diffToolContent = toolCall.input.diff
|
|
|
|
if (!diffToolPath || !diffToolContent) {
|
|
return {
|
|
success: false,
|
|
streamResult: streamResult,
|
|
toolCalls: detectedToolCalls,
|
|
error: "tool_call_params_undefined",
|
|
}
|
|
}
|
|
|
|
// check that we are editing the correct file path
|
|
log(input.isVerbose, `Expected file path: "${originalFilePath}"`)
|
|
log(input.isVerbose, `Actual file path used: "${diffToolPath}"`)
|
|
if (diffToolPath !== originalFilePath) {
|
|
log(input.isVerbose, `❌ File path mismatch detected!`)
|
|
// Enhanced logging:
|
|
if (streamResult?.assistantMessage) {
|
|
log(input.isVerbose, ` Full model output (assistantMessage):`)
|
|
log(input.isVerbose, ` -----------------------------------------`)
|
|
log(input.isVerbose, ` ${streamResult.assistantMessage}`)
|
|
log(input.isVerbose, ` -----------------------------------------`)
|
|
}
|
|
if (toolCall) {
|
|
log(input.isVerbose, ` Parsed tool call that caused mismatch:`)
|
|
log(input.isVerbose, ` ${JSON.stringify(toolCall, null, 2)}`)
|
|
log(input.isVerbose, ` -----------------------------------------`)
|
|
}
|
|
return {
|
|
success: false,
|
|
streamResult: streamResult,
|
|
toolCalls: detectedToolCalls,
|
|
error: "wrong_file_edited",
|
|
}
|
|
}
|
|
|
|
// checking if the diff edit succeeds, if it failed it will throw an error
|
|
let diffSuccess = true
|
|
let replacementData: any = undefined
|
|
try {
|
|
const result = await constructNewFileContent(diffToolContent, originalFile, true)
|
|
|
|
// Check if result is an object with replacements (new format)
|
|
if (typeof result === 'object' && result !== null && 'replacements' in result) {
|
|
replacementData = result.replacements
|
|
}
|
|
// If it's just a string, diffSuccess stays true and replacementData stays undefined
|
|
} catch (error: any) {
|
|
diffSuccess = false
|
|
log(input.isVerbose, `ERROR: ${error}`)
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
streamResult: streamResult,
|
|
toolCalls: detectedToolCalls,
|
|
diffEdit: diffToolContent,
|
|
diffEditSuccess: diffSuccess,
|
|
replacementData: replacementData,
|
|
}
|
|
} catch (error: any) {
|
|
return {
|
|
success: false,
|
|
error: "other_error",
|
|
errorString: error.message || error.toString(),
|
|
}
|
|
}
|
|
}
|