refactor(providers): split buildPrompt + extract applyCliDefaults to keep complexity under ratchet (#6220) (#5997)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-05 00:50:46 -03:00
parent cda5917160
commit 0a29607bb4
2 changed files with 82 additions and 71 deletions

View File

@@ -91,79 +91,74 @@ function extractTextContent(content: unknown): string {
* re-emitting the same `<tool>` 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 `<tool>` 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({

View File

@@ -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<string, string>,
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();
}