From 5d5e170c020c6033b6addc763aecd8c99fbe0366 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 31 Mar 2026 22:10:31 -0400 Subject: [PATCH] test: simplify mock llm responses renderer --- packages/opencode/test/lib/llm-server.ts | 413 ++++++++++++----------- 1 file changed, 224 insertions(+), 189 deletions(-) diff --git a/packages/opencode/test/lib/llm-server.ts b/packages/opencode/test/lib/llm-server.ts index d651140b31..fb84f1175a 100644 --- a/packages/opencode/test/lib/llm-server.ts +++ b/packages/opencode/test/lib/llm-server.ts @@ -8,6 +8,13 @@ export type Usage = { input: number; output: number } type Line = Record +type Flow = + | { type: "text"; text: string } + | { type: "reason"; text: string } + | { type: "tool-start"; id: string; name: string } + | { type: "tool-args"; text: string } + | { type: "usage"; usage: Usage } + type Hit = { url: URL body: Record @@ -119,7 +126,7 @@ function bytes(input: Iterable) { return Stream.fromIterable([...input].map(line)).pipe(Stream.encodeText) } -function created(model: string) { +function responseCreated(model: string) { return { type: "response.created", sequence_number: 1, @@ -132,7 +139,7 @@ function created(model: string) { } } -function completed(input: { seq: number; usage?: Usage }) { +function responseCompleted(input: { seq: number; usage?: Usage }) { return { type: "response.completed", sequence_number: input.seq, @@ -149,10 +156,168 @@ function completed(input: { seq: number; usage?: Usage }) { } } +function responseMessage(id: string, seq: number) { + return { + type: "response.output_item.added", + sequence_number: seq, + output_index: 0, + item: { type: "message", id }, + } +} + +function responseText(id: string, text: string, seq: number) { + return { + type: "response.output_text.delta", + sequence_number: seq, + item_id: id, + delta: text, + logprobs: null, + } +} + +function responseMessageDone(id: string, seq: number) { + return { + type: "response.output_item.done", + sequence_number: seq, + output_index: 0, + item: { type: "message", id }, + } +} + +function responseReason(id: string, seq: number) { + return { + type: "response.output_item.added", + sequence_number: seq, + output_index: 0, + item: { type: "reasoning", id, encrypted_content: null }, + } +} + +function responseReasonPart(id: string, seq: number) { + return { + type: "response.reasoning_summary_part.added", + sequence_number: seq, + item_id: id, + summary_index: 0, + } +} + +function responseReasonText(id: string, text: string, seq: number) { + return { + type: "response.reasoning_summary_text.delta", + sequence_number: seq, + item_id: id, + summary_index: 0, + delta: text, + } +} + +function responseReasonDone(id: string, seq: number) { + return { + type: "response.output_item.done", + sequence_number: seq, + output_index: 0, + item: { type: "reasoning", id, encrypted_content: null }, + } +} + +function responseTool(id: string, item: string, name: string, seq: number) { + return { + type: "response.output_item.added", + sequence_number: seq, + output_index: 0, + item: { + type: "function_call", + id: item, + call_id: id, + name, + arguments: "", + status: "in_progress", + }, + } +} + +function responseToolArgs(id: string, text: string, seq: number) { + return { + type: "response.function_call_arguments.delta", + sequence_number: seq, + output_index: 0, + item_id: id, + delta: text, + } +} + +function responseToolDone(tool: { id: string; item: string; name: string; args: string }, seq: number) { + return { + type: "response.output_item.done", + sequence_number: seq, + output_index: 0, + item: { + type: "function_call", + id: tool.item, + call_id: tool.id, + name: tool.name, + arguments: tool.args, + status: "completed", + }, + } +} + +function choices(part: unknown) { + if (!part || typeof part !== "object") return + if (!("choices" in part) || !Array.isArray(part.choices)) return + const choice = part.choices[0] + if (!choice || typeof choice !== "object") return + return choice +} + +function flow(item: Sse) { + const out: Flow[] = [] + for (const part of [...item.head, ...item.tail]) { + const choice = choices(part) + const delta = + choice && "delta" in choice && choice.delta && typeof choice.delta === "object" ? choice.delta : undefined + + if (delta && "content" in delta && typeof delta.content === "string") { + out.push({ type: "text", text: delta.content }) + } + + if (delta && "reasoning_content" in delta && typeof delta.reasoning_content === "string") { + out.push({ type: "reason", text: delta.reasoning_content }) + } + + if (delta && "tool_calls" in delta && Array.isArray(delta.tool_calls)) { + for (const tool of delta.tool_calls) { + if (!tool || typeof tool !== "object") continue + const fn = "function" in tool && tool.function && typeof tool.function === "object" ? tool.function : undefined + if ("id" in tool && typeof tool.id === "string" && fn && "name" in fn && typeof fn.name === "string") { + out.push({ type: "tool-start", id: tool.id, name: fn.name }) + } + if (fn && "arguments" in fn && typeof fn.arguments === "string" && fn.arguments) { + out.push({ type: "tool-args", text: fn.arguments }) + } + } + } + + if (part && typeof part === "object" && "usage" in part && part.usage && typeof part.usage === "object") { + const raw = part.usage as Record + if (typeof raw.prompt_tokens === "number" && typeof raw.completion_tokens === "number") { + out.push({ + type: "usage", + usage: { input: raw.prompt_tokens, output: raw.completion_tokens }, + }) + } + } + } + return out +} + function responses(item: Sse, model: string) { let seq = 1 let msg: string | undefined let reason: string | undefined + let hasMsg = false + let hasReason = false let call: | { id: string @@ -162,173 +327,66 @@ function responses(item: Sse, model: string) { } | undefined let usage: Usage | undefined - const lines: unknown[] = [created(model)] + const lines: unknown[] = [responseCreated(model)] - const all = [...item.head, ...item.tail] - for (const part of all) { - if (!part || typeof part !== "object") continue - if (!("choices" in part) || !Array.isArray(part.choices)) continue - const choice = part.choices[0] - if (!choice || typeof choice !== "object") continue - const delta = "delta" in choice && choice.delta && typeof choice.delta === "object" ? choice.delta : undefined - - if (delta && "content" in delta && typeof delta.content === "string") { - msg ||= "msg_1" - if ( - !lines.some( - (item) => - typeof item === "object" && - item && - "type" in item && - item.type === "response.output_item.added" && - "item" in item && - item.item && - typeof item.item === "object" && - "id" in item.item && - item.item.id === msg, - ) - ) { + for (const part of flow(item)) { + if (part.type === "text") { + msg ??= "msg_1" + if (!hasMsg) { + hasMsg = true seq += 1 - lines.push({ - type: "response.output_item.added", - sequence_number: seq, - output_index: 0, - item: { type: "message", id: msg }, - }) + lines.push(responseMessage(msg, seq)) } seq += 1 - lines.push({ - type: "response.output_text.delta", - sequence_number: seq, - item_id: msg, - delta: delta.content, - logprobs: null, - }) + lines.push(responseText(msg, part.text, seq)) + continue } - if (delta && "reasoning_content" in delta && typeof delta.reasoning_content === "string") { + if (part.type === "reason") { reason ||= "rs_1" - if ( - !lines.some( - (item) => - typeof item === "object" && - item && - "type" in item && - item.type === "response.output_item.added" && - "item" in item && - item.item && - typeof item.item === "object" && - "id" in item.item && - item.item.id === reason, - ) - ) { + if (!hasReason) { + hasReason = true seq += 1 - lines.push({ - type: "response.output_item.added", - sequence_number: seq, - output_index: 0, - item: { type: "reasoning", id: reason, encrypted_content: null }, - }) + lines.push(responseReason(reason, seq)) seq += 1 - lines.push({ - type: "response.reasoning_summary_part.added", - sequence_number: seq, - item_id: reason, - summary_index: 0, - }) + lines.push(responseReasonPart(reason, seq)) } seq += 1 - lines.push({ - type: "response.reasoning_summary_text.delta", - sequence_number: seq, - item_id: reason, - summary_index: 0, - delta: delta.reasoning_content, - }) + lines.push(responseReasonText(reason, part.text, seq)) + continue } - if (delta && "tool_calls" in delta && Array.isArray(delta.tool_calls)) { - for (const tool of delta.tool_calls) { - if (!tool || typeof tool !== "object") continue - const fn = "function" in tool && tool.function && typeof tool.function === "object" ? tool.function : undefined - const id = "id" in tool && typeof tool.id === "string" ? tool.id : call?.id - const name = fn && "name" in fn && typeof fn.name === "string" ? fn.name : call?.name - const args = fn && "arguments" in fn && typeof fn.arguments === "string" ? fn.arguments : "" - if (!id || !name) continue - if (!call) { - call = { id, item: "fc_1", name, args: "" } - seq += 1 - lines.push({ - type: "response.output_item.added", - sequence_number: seq, - output_index: 0, - item: { - type: "function_call", - id: call.item, - call_id: id, - name, - arguments: "", - status: "in_progress", - }, - }) - } - call.args += args - if (args) { - seq += 1 - lines.push({ - type: "response.function_call_arguments.delta", - sequence_number: seq, - output_index: 0, - item_id: call.item, - delta: args, - }) - } - } + if (part.type === "tool-start") { + call ||= { id: part.id, item: "fc_1", name: part.name, args: "" } + seq += 1 + lines.push(responseTool(call.id, call.item, call.name, seq)) + continue } - if ("usage" in part && part.usage && typeof part.usage === "object") { - const raw = part.usage as Record - if (typeof raw.prompt_tokens === "number" && typeof raw.completion_tokens === "number") { - usage = { input: raw.prompt_tokens, output: raw.completion_tokens } - } + if (part.type === "tool-args") { + if (!call) continue + call.args += part.text + seq += 1 + lines.push(responseToolArgs(call.item, part.text, seq)) + continue } + + usage = part.usage } if (msg) { seq += 1 - lines.push({ - type: "response.output_item.done", - sequence_number: seq, - output_index: 0, - item: { type: "message", id: msg }, - }) + lines.push(responseMessageDone(msg, seq)) } if (reason) { seq += 1 - lines.push({ - type: "response.output_item.done", - sequence_number: seq, - output_index: 0, - item: { type: "reasoning", id: reason, encrypted_content: null }, - }) + lines.push(responseReasonDone(reason, seq)) } if (call && !item.hang && !item.error) { seq += 1 - lines.push({ - type: "response.output_item.done", - sequence_number: seq, - output_index: 0, - item: { - type: "function_call", - id: call.item, - call_id: call.id, - name: call.name, - arguments: call.args, - status: "completed", - }, - }) + lines.push(responseToolDone(call, seq)) } - if (!item.hang && !item.error) lines.push(completed({ seq: seq + 1, usage })) + if (!item.hang && !item.error) lines.push(responseCompleted({ seq: seq + 1, usage })) return { ...item, head: lines, tail: [] } satisfies Sse } @@ -512,6 +570,13 @@ function item(input: Item | Reply) { return input instanceof Reply ? input.item() : input } +function hit(url: string, body: unknown) { + return { + url: new URL(url, "http://localhost"), + body: body && typeof body === "object" ? (body as Record) : {}, + } satisfies Hit +} + namespace TestLLMServer { export interface Service { readonly url: string @@ -561,54 +626,24 @@ export class TestLLMServer extends ServiceMap.Service ({}))) - hits = [ - ...hits, - { - url: new URL(req.originalUrl, "http://localhost"), - body: body && typeof body === "object" ? (body as Record) : {}, - }, - ] - yield* notify() - if (req.originalUrl.endsWith("/v1/responses") && next.type === "sse") { - return send(responses(next, modelFrom(body))) - } - if (next.type === "sse" && next.reset) { - yield* reset(next) - return HttpServerResponse.empty() - } - if (next.type === "sse") return send(next) - return fail(next) - }), - ) + const handle = Effect.fn("TestLLMServer.handle")(function* (mode: "chat" | "responses") { + const req = yield* HttpServerRequest.HttpServerRequest + const next = pull() + if (!next) return HttpServerResponse.text("unexpected request", { status: 500 }) + const body = yield* req.json.pipe(Effect.orElseSucceed(() => ({}))) + hits = [...hits, hit(req.originalUrl, body)] + yield* notify() + if (next.type !== "sse") return fail(next) + if (mode === "responses") return send(responses(next, modelFrom(body))) + if (next.reset) { + yield* reset(next) + return HttpServerResponse.empty() + } + return send(next) + }) - yield* router.add( - "POST", - "/v1/responses", - Effect.gen(function* () { - const req = yield* HttpServerRequest.HttpServerRequest - const next = pull() - if (!next) return HttpServerResponse.text("unexpected request", { status: 500 }) - const body = yield* req.json.pipe(Effect.orElseSucceed(() => ({}))) - hits = [ - ...hits, - { - url: new URL(req.originalUrl, "http://localhost"), - body: body && typeof body === "object" ? (body as Record) : {}, - }, - ] - yield* notify() - if (next.type === "sse") return send(responses(next, modelFrom(body))) - return fail(next) - }), - ) + yield* router.add("POST", "/v1/chat/completions", handle("chat")) + yield* router.add("POST", "/v1/responses", handle("responses")) yield* server.serve(router.asHttpEffect())