From 0a29607bb4bcf10e9c4b655e2a3a6004f5945c34 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Sun, 5 Jul 2026 00:50:46 -0300 Subject: [PATCH] refactor(providers): split buildPrompt + extract applyCliDefaults to keep complexity under ratchet (#6220) (#5997) --- open-sse/executors/gitlab.ts | 117 ++++++++++++++++-------------- open-sse/utils/opencodeHeaders.ts | 36 ++++----- 2 files changed, 82 insertions(+), 71 deletions(-) diff --git a/open-sse/executors/gitlab.ts b/open-sse/executors/gitlab.ts index 2c78828c1c..510a8a39e3 100644 --- a/open-sse/executors/gitlab.ts +++ b/open-sse/executors/gitlab.ts @@ -91,79 +91,74 @@ function extractTextContent(content: unknown): string { * re-emitting the same `` call forever (#6220). Complements the tool_call emission * added in #6051. */ -export function buildPrompt(messages: OpenAIMessage[] | undefined): string { - if (!Array.isArray(messages)) return ""; - - const hasToolExchange = messages.some((message) => { +function hasToolExchange(messages: OpenAIMessage[]): boolean { + return messages.some((message) => { const role = String(message?.role || "user").toLowerCase(); if (role === "tool") return true; return ( - role === "assistant" && - Array.isArray(message?.tool_calls) && - message.tool_calls.length > 0 + role === "assistant" && Array.isArray(message?.tool_calls) && message.tool_calls.length > 0 ); }); +} +/** Legacy flatten: system instructions + the latest user message. */ +function buildSimplePrompt(messages: OpenAIMessage[]): string { const systemParts: string[] = []; - - if (!hasToolExchange) { - // Legacy path — unchanged: system instructions + the latest user message. - const userParts: string[] = []; - for (const message of messages) { - const role = String(message?.role || "user").toLowerCase(); - const text = extractTextContent(message?.content); - if (!text) continue; - if (role === "system" || role === "developer") { - systemParts.push(text); - continue; - } - if (role === "user") { - userParts.push(text); - } + const userParts: string[] = []; + for (const message of messages) { + const role = String(message?.role || "user").toLowerCase(); + const text = extractTextContent(message?.content); + if (!text) continue; + if (role === "system" || role === "developer") { + systemParts.push(text); + } else if (role === "user") { + userParts.push(text); } - const latestUserPrompt = userParts.at(-1) || ""; - if (!systemParts.length) { - return latestUserPrompt; - } - return `System instructions:\n${systemParts.join("\n\n")}\n\n${latestUserPrompt}`.trim(); } + const latestUserPrompt = userParts.at(-1) || ""; + if (!systemParts.length) return latestUserPrompt; + return `System instructions:\n${systemParts.join("\n\n")}\n\n${latestUserPrompt}`.trim(); +} - // Tool-exchange path — serialize the full turn history so the model sees the tool - // result and continues instead of repeating the tool call (#6220). +/** Render an assistant turn (its text plus any tool calls) for the tool-exchange prompt. */ +function renderAssistantTurn(message: OpenAIMessage, text: string): string | null { + const lines: string[] = []; + if (text) lines.push(text); + for (const tc of Array.isArray(message?.tool_calls) ? message.tool_calls : []) { + const id = tc?.id ? ` [${tc.id}]` : ""; + lines.push( + `Called tool ${tc?.function?.name || "tool"}${id} with arguments: ${tc?.function?.arguments ?? ""}` + ); + } + return lines.length ? `Assistant: ${lines.join("\n")}` : null; +} + +/** Render one message as a labeled conversation line for the tool-exchange prompt. */ +function renderConversationTurn(message: OpenAIMessage, role: string, text: string): string | null { + if (role === "user") return text ? `User: ${text}` : null; + if (role === "assistant") return renderAssistantTurn(message, text); + if (role === "tool") { + const id = message?.tool_call_id ? ` for ${message.tool_call_id}` : ""; + const name = message?.name ? ` (${message.name})` : ""; + return `Tool result${name}${id}: ${text}`; + } + return null; +} + +/** Serialize the full turn history so the model sees the tool result (#6220). */ +function buildToolExchangePrompt(messages: OpenAIMessage[]): string { + const systemParts: string[] = []; const convo: string[] = []; for (const message of messages) { const role = String(message?.role || "user").toLowerCase(); const text = extractTextContent(message?.content); - if (role === "system" || role === "developer") { if (text) systemParts.push(text); continue; } - if (role === "user") { - if (text) convo.push(`User: ${text}`); - continue; - } - if (role === "assistant") { - const lines: string[] = []; - if (text) lines.push(text); - const toolCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : []; - for (const tc of toolCalls) { - const name = tc?.function?.name || "tool"; - const args = tc?.function?.arguments ?? ""; - const id = tc?.id ? ` [${tc.id}]` : ""; - lines.push(`Called tool ${name}${id} with arguments: ${args}`); - } - if (lines.length) convo.push(`Assistant: ${lines.join("\n")}`); - continue; - } - if (role === "tool") { - const id = message?.tool_call_id ? ` for ${message.tool_call_id}` : ""; - const name = message?.name ? ` (${message.name})` : ""; - convo.push(`Tool result${name}${id}: ${text}`); - continue; - } + const line = renderConversationTurn(message, role, text); + if (line) convo.push(line); } - const header = systemParts.length ? `System instructions:\n${systemParts.join("\n\n")}\n\n` : ""; @@ -172,6 +167,20 @@ export function buildPrompt(messages: OpenAIMessage[] | undefined): string { )}\n\nContinue the response using the tool result above; do not repeat the tool call.`.trim(); } +/** + * GitLab code_suggestions is a single-prompt completion API (no chat roles), so the + * OpenAI message array must be flattened to text. Simple conversations keep the legacy + * shape; a tool exchange (assistant `tool_calls` or a `tool` result) serializes the full + * conversation so the model continues instead of re-emitting the same `` call + * forever (#6220). Complements the tool_call emission added in #6051. + */ +export function buildPrompt(messages: OpenAIMessage[] | undefined): string { + if (!Array.isArray(messages)) return ""; + return hasToolExchange(messages) + ? buildToolExchangePrompt(messages) + : buildSimplePrompt(messages); +} + function toOpenAIError(status: number, message: string): Response { return new Response( JSON.stringify({ diff --git a/open-sse/utils/opencodeHeaders.ts b/open-sse/utils/opencodeHeaders.ts index a871488c47..4e1221877c 100644 --- a/open-sse/utils/opencodeHeaders.ts +++ b/open-sse/utils/opencodeHeaders.ts @@ -75,22 +75,24 @@ export function forwardOpencodeClientHeaders( // 4. OpencodeExecutor-only: synthesize the OpenCode CLI identity Cloudflare expects // on VPS egress, for any key the client did not supply (#5997). - const cliDefaults = options?.cliDefaults; - if (cliDefaults) { - if (!headers["User-Agent"] && !headers["user-agent"]) { - setUserAgentHeader(headers, cliDefaults.userAgent); - } - if (!headers["x-opencode-client"]) { - headers["x-opencode-client"] = cliDefaults.client; - } - if (!headers["x-opencode-project"]) { - headers["x-opencode-project"] = cliDefaults.project; - } - if (!headers["x-opencode-request"]) { - headers["x-opencode-request"] = randomUUID(); - } - if (!headers["x-opencode-session"]) { - headers["x-opencode-session"] = randomUUID(); - } + if (options?.cliDefaults) { + applyCliDefaults(headers, options.cliDefaults); } } + +/** + * Fill the OpenCode CLI identity headers Cloudflare requires on VPS egress, but only for + * keys the client did not already supply (client values always win). (#5997) + */ +function applyCliDefaults( + headers: Record, + cliDefaults: { userAgent: string; client: string; project: string } +): void { + if (!headers["User-Agent"] && !headers["user-agent"]) { + setUserAgentHeader(headers, cliDefaults.userAgent); + } + headers["x-opencode-client"] ||= cliDefaults.client; + headers["x-opencode-project"] ||= cliDefaults.project; + headers["x-opencode-request"] ||= randomUUID(); + headers["x-opencode-session"] ||= randomUUID(); +}