mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 03:56:55 +03:00
fix(ai): mark replayed assistant messages completed (#48221)
This commit is contained in:
committed by
GitHub
Unverified
parent
c45e425e12
commit
1bd85d926b
@@ -655,7 +655,8 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
|
||||
type: "message" as const,
|
||||
...(group.id === undefined ? {} : { id: group.id }),
|
||||
role: "assistant" as const,
|
||||
status: metadata?.status,
|
||||
// Replayed text is a finished input item, even if generation was cut short.
|
||||
status: "completed",
|
||||
content: group.parts.map((part) => ({ type: "output_text" as const, text: part.text })),
|
||||
...(group.phase === undefined ? {} : { phase: group.phase }),
|
||||
})),
|
||||
|
||||
+6
-6
File diff suppressed because one or more lines are too long
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+4
-4
File diff suppressed because one or more lines are too long
+6
-6
File diff suppressed because one or more lines are too long
+1
-1
@@ -81,7 +81,7 @@
|
||||
{
|
||||
"direction": "client",
|
||||
"kind": "text",
|
||||
"body": "{\"type\":\"response.create\",\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Alpha.\"}]},{\"type\":\"message\",\"id\":\"msg_ws_reconnect_1\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Alpha.\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Beta.\"}]}],\"store\":false,\"max_output_tokens\":30,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"instructions\":\"Follow the user's exact reply instruction.\"}"
|
||||
"body": "{\"type\":\"response.create\",\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Alpha.\"}]},{\"type\":\"message\",\"id\":\"msg_ws_reconnect_1\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"Alpha.\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Beta.\"}]}],\"store\":false,\"max_output_tokens\":30,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"instructions\":\"Follow the user's exact reply instruction.\"}"
|
||||
},
|
||||
{
|
||||
"direction": "server",
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@
|
||||
{
|
||||
"direction": "client",
|
||||
"kind": "text",
|
||||
"body": "{\"type\":\"response.create\",\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Ready.\"}]},{\"type\":\"message\",\"id\":\"msg_ws_rejection_1\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Ready.\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Recovered.\"}]}],\"store\":false,\"max_output_tokens\":30,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"instructions\":\"Follow the user's exact reply instruction.\"}"
|
||||
"body": "{\"type\":\"response.create\",\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Ready.\"}]},{\"type\":\"message\",\"id\":\"msg_ws_rejection_1\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"Ready.\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Recovered.\"}]}],\"store\":false,\"max_output_tokens\":30,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"instructions\":\"Follow the user's exact reply instruction.\"}"
|
||||
},
|
||||
{
|
||||
"direction": "server",
|
||||
|
||||
+5
-5
File diff suppressed because one or more lines are too long
@@ -23,7 +23,7 @@ it.effect("conversation lowering excludes generation settings and tool definitio
|
||||
instructions: "Keep the context",
|
||||
input: [
|
||||
{ role: "user", content: [{ type: "input_text", text: "hello" }] },
|
||||
{ type: "message", role: "assistant", content: [{ type: "output_text", text: "hi" }] },
|
||||
{ type: "message", role: "assistant", status: "completed", content: [{ type: "output_text", text: "hi" }] },
|
||||
],
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMEvent, Message } from "../../src/index.js"
|
||||
import { OpenAI } from "../../src/providers.js"
|
||||
import { configure } from "../../src/providers/openai-compatible-responses.js"
|
||||
import { compileRequest, LLMClient } from "../../src/route/client.js"
|
||||
import { it } from "../lib/effect.js"
|
||||
import { fixedResponse } from "../lib/http.js"
|
||||
import { sseEvents } from "../lib/sse.js"
|
||||
|
||||
for (const model of [
|
||||
OpenAI.configure({ apiKey: "test-key" }).responses("example-model"),
|
||||
configure({ apiKey: "test-key", baseURL: "https://responses.example.test/v1" }).model("example-model"),
|
||||
]) {
|
||||
describe(`${model.route.protocol} message replay`, () => {
|
||||
const key = model.route.providerMetadataKey ?? "openresponses"
|
||||
|
||||
it.effect("marks assistant text completed regardless of stored status", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
...[undefined, "in_progress", "incomplete", "completed"].map((status, index) =>
|
||||
Message.make({
|
||||
role: "assistant",
|
||||
providerMetadata: { [key]: { status } },
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Saved ${index}`,
|
||||
providerMetadata: { [key]: { itemId: `msg_${index}`, phase: "commentary", status } },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: `Final ${index}`,
|
||||
providerMetadata: { [key]: { itemId: `msg_final_${index}`, phase: "final_answer", status } },
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
Message.make({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Continue" }],
|
||||
providerMetadata: { [key]: { status: "incomplete" } },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
expect(prepared.body.input).toEqual([
|
||||
...[0, 1, 2, 3].flatMap((index) => [
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
id: `msg_${index}`,
|
||||
phase: "commentary",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: `Saved ${index}` }],
|
||||
},
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
id: `msg_final_${index}`,
|
||||
phase: "final_answer",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: `Final ${index}` }],
|
||||
},
|
||||
]),
|
||||
{ role: "user", status: "incomplete", content: [{ type: "input_text", text: "Continue" }] },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("replays truncated text as completed while retaining the response finish reason", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(LLM.request({ model, prompt: "Respond" })).pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(
|
||||
sseEvents(
|
||||
{
|
||||
type: "response.output_item.added",
|
||||
item: { type: "message", id: "msg_partial", status: "in_progress" },
|
||||
},
|
||||
{ type: "response.output_text.delta", item_id: "msg_partial", delta: "The next step is" },
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
item: {
|
||||
type: "message",
|
||||
id: "msg_partial",
|
||||
status: "incomplete",
|
||||
content: [{ type: "output_text", text: "The next step is" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "response.incomplete",
|
||||
response: { status: "incomplete", incomplete_details: { reason: "max_output_tokens" } },
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
expect(response.finishReason.normalized).toBe("length")
|
||||
expect(response.events.filter(LLMEvent.is.textEnd)).toHaveLength(1)
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({ model, messages: [response.message, Message.user("Continue")] }),
|
||||
)
|
||||
expect(prepared.body.input).toEqual([
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
id: "msg_partial",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: "The next step is" }],
|
||||
},
|
||||
{ role: "user", content: [{ type: "input_text", text: "Continue" }] },
|
||||
])
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -91,7 +91,7 @@ describe("Open Responses-compatible route", () => {
|
||||
expect(prepared.body.input).toEqual([
|
||||
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
|
||||
{ role: "developer", content: "Operator update." },
|
||||
{ type: "message", role: "assistant", content: [{ type: "output_text", text: "After." }] },
|
||||
{ type: "message", role: "assistant", status: "completed", content: [{ type: "output_text", text: "After." }] },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -299,23 +299,27 @@ describe("Open Responses-compatible route", () => {
|
||||
type: "message",
|
||||
id: "history_1",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: "Kept." }],
|
||||
},
|
||||
{
|
||||
type: "message",
|
||||
id: `history_${"a".repeat(64)}`,
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: "Long." }],
|
||||
},
|
||||
{
|
||||
type: "message",
|
||||
id: "provider_value/with+symbols",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: "Opaque." }],
|
||||
},
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [
|
||||
{ type: "output_text", text: "No suffix." },
|
||||
{ type: "output_text", text: "No prefix." },
|
||||
@@ -856,6 +860,7 @@ describe("Open Responses-compatible route", () => {
|
||||
type: "message",
|
||||
id: "msg_refusal",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: "I can't help with that." }],
|
||||
},
|
||||
])
|
||||
|
||||
@@ -171,7 +171,7 @@ describe("OpenAI Responses WebSocket recorded", () => {
|
||||
instructions: "Follow the user's exact reply instruction.",
|
||||
input: [
|
||||
{ role: "user", content: [{ type: "input_text", text: "Reply exactly: Alpha." }] },
|
||||
{ role: "assistant", content: [{ type: "output_text", text: "Alpha." }] },
|
||||
{ role: "assistant", status: "completed", content: [{ type: "output_text", text: "Alpha." }] },
|
||||
{ role: "user", content: [{ type: "input_text", text: "Reply exactly: Beta." }] },
|
||||
],
|
||||
})
|
||||
@@ -208,7 +208,7 @@ describe("OpenAI Responses WebSocket recorded", () => {
|
||||
instructions: "Follow the user's exact reply instruction.",
|
||||
input: [
|
||||
{ role: "user", content: [{ type: "input_text", text: "Reply exactly: Ready." }] },
|
||||
{ role: "assistant", content: [{ type: "output_text", text: "Ready." }] },
|
||||
{ role: "assistant", status: "completed", content: [{ type: "output_text", text: "Ready." }] },
|
||||
{ role: "user", content: [{ type: "input_text", text: "Reply exactly: Recovered." }] },
|
||||
],
|
||||
})
|
||||
|
||||
@@ -406,7 +406,7 @@ describe("OpenAI Responses route", () => {
|
||||
expect(prepared.body.input).toEqual([
|
||||
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
|
||||
{ role: "developer", content: "Operator update." },
|
||||
{ type: "message", role: "assistant", content: [{ type: "output_text", text: "After." }] },
|
||||
{ type: "message", role: "assistant", status: "completed", content: [{ type: "output_text", text: "After." }] },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -2118,6 +2118,7 @@ describe("OpenAI Responses route", () => {
|
||||
type: "message",
|
||||
id: "msg_refusal",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: "I can't help with that." }],
|
||||
phase: "final_answer",
|
||||
},
|
||||
@@ -2200,6 +2201,7 @@ describe("OpenAI Responses route", () => {
|
||||
type: "message",
|
||||
id: "msg_commentary",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: "Checking." }],
|
||||
phase: "commentary",
|
||||
},
|
||||
@@ -2207,6 +2209,7 @@ describe("OpenAI Responses route", () => {
|
||||
type: "message",
|
||||
id: "msg_final",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: "Finished." }],
|
||||
phase: "final_answer",
|
||||
},
|
||||
@@ -2214,6 +2217,7 @@ describe("OpenAI Responses route", () => {
|
||||
type: "message",
|
||||
id: "msg_null",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: "Unclassified." }],
|
||||
phase: null,
|
||||
},
|
||||
@@ -3344,14 +3348,19 @@ describe("OpenAI Responses route", () => {
|
||||
)
|
||||
|
||||
expect(prepared.body.input).toEqual([
|
||||
{ type: "message", role: "assistant", content: [{ type: "output_text", text: "Before." }] },
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: "Before." }],
|
||||
},
|
||||
{
|
||||
type: "reasoning",
|
||||
id: "rs_1",
|
||||
encrypted_content: "encrypted-state",
|
||||
summary: [{ type: "summary_text", text: "Checked order." }],
|
||||
},
|
||||
{ type: "message", role: "assistant", content: [{ type: "output_text", text: "After." }] },
|
||||
{ type: "message", role: "assistant", status: "completed", content: [{ type: "output_text", text: "After." }] },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -3615,12 +3624,14 @@ describe("OpenAI Responses route", () => {
|
||||
type: "message",
|
||||
id: "history_1",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: "Hello" }],
|
||||
},
|
||||
{
|
||||
type: "message",
|
||||
id: `message_${"a".repeat(64)}`,
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: "World" }],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user