mirror of
https://github.com/Heretek-AI/opencode-4-openclaude.git
synced 2026-11-07 02:45:58 -10:00
feat: stream reasoning summary deltas as reasoning_content for Muse Spark 1.3
This commit is contained in:
1 parent
c138a9c8f3
commit
799dcfc316
3 files changed
+121
-9
No files matched your search
+49
-6
@@ -123,12 +123,17 @@ export function chatCompletionsToResponsesBody(body) {
|
||||
if (typeof max_output_tokens === "number") responsesPayload.max_output_tokens = max_output_tokens;
|
||||
else if (typeof max_tokens === "number") responsesPayload.max_output_tokens = max_tokens;
|
||||
|
||||
// Translate reasoning_effort (OpenAI Chat Completions) to reasoning: { effort } (Responses API)
|
||||
// Translate reasoning_effort (OpenAI Chat Completions) to reasoning: { effort, summary } (Responses API)
|
||||
const incomingEffort = body.reasoning_effort || (typeof body.reasoning === "object" ? body.reasoning?.effort : undefined);
|
||||
if (incomingEffort && incomingEffort !== "auto" && incomingEffort !== "none") {
|
||||
responsesPayload.reasoning = {
|
||||
effort: incomingEffort
|
||||
if (incomingEffort !== "none") {
|
||||
const incomingSummary = typeof body.reasoning === "object" && body.reasoning?.summary ? body.reasoning.summary : "detailed";
|
||||
const reasoningObj = {
|
||||
summary: incomingSummary
|
||||
};
|
||||
if (incomingEffort && incomingEffort !== "auto") {
|
||||
reasoningObj.effort = incomingEffort;
|
||||
}
|
||||
responsesPayload.reasoning = reasoningObj;
|
||||
}
|
||||
|
||||
if (Array.isArray(tools)) {
|
||||
@@ -171,11 +176,23 @@ export function responsesToChatCompletionsResponse(responsesData, requestedModel
|
||||
const model = responsesData.model || requestedModel;
|
||||
|
||||
let textContent = "";
|
||||
let reasoningContent = "";
|
||||
const toolCalls = [];
|
||||
|
||||
const output = Array.isArray(responsesData.output) ? responsesData.output : [];
|
||||
for (const item of output) {
|
||||
if (item.type === "message") {
|
||||
if (item.type === "reasoning") {
|
||||
if (Array.isArray(item.summary)) {
|
||||
const parts = item.summary.map(s => s.text || s.content || (typeof s === "string" ? s : "")).filter(Boolean);
|
||||
if (parts.length > 0) {
|
||||
reasoningContent = parts.join("\n\n");
|
||||
}
|
||||
} else if (typeof item.summary === "string") {
|
||||
reasoningContent = item.summary;
|
||||
} else if (typeof item.content === "string") {
|
||||
reasoningContent = item.content;
|
||||
}
|
||||
} else if (item.type === "message") {
|
||||
if (Array.isArray(item.content)) {
|
||||
for (const part of item.content) {
|
||||
if ((part.type === "output_text" || part.type === "text") && typeof part.text === "string") {
|
||||
@@ -205,7 +222,8 @@ export function responsesToChatCompletionsResponse(responsesData, requestedModel
|
||||
|
||||
const message = {
|
||||
role: "assistant",
|
||||
content: textContent || (toolCalls.length ? null : "")
|
||||
content: textContent || (toolCalls.length ? null : ""),
|
||||
...(reasoningContent ? { reasoning_content: reasoningContent } : {})
|
||||
};
|
||||
if (toolCalls.length > 0) {
|
||||
message.tool_calls = toolCalls;
|
||||
@@ -280,6 +298,31 @@ export function translateResponsesEventToOpenAiChunk(eventString, state = { id:
|
||||
choices: []
|
||||
};
|
||||
|
||||
// Reasoning Summary / Reasoning Text Delta
|
||||
if (
|
||||
eventType === "response.reasoning_summary_text.delta" ||
|
||||
eventType === "response.reasoning_text.delta" ||
|
||||
data.type === "response.reasoning_summary_text.delta" ||
|
||||
data.type === "response.reasoning_text.delta"
|
||||
) {
|
||||
let reasoningText = data.delta ?? data.text ?? data.reasoning_delta ?? "";
|
||||
if (reasoningText) {
|
||||
state.hasEmittedReasoning = true;
|
||||
if (typeof data.summary_index === "number") {
|
||||
if (state.lastSummaryIndex !== undefined && state.lastSummaryIndex !== data.summary_index) {
|
||||
reasoningText = "\n\n" + reasoningText;
|
||||
}
|
||||
state.lastSummaryIndex = data.summary_index;
|
||||
}
|
||||
baseChunk.choices.push({
|
||||
index: 0,
|
||||
delta: { reasoning_content: reasoningText },
|
||||
finish_reason: null
|
||||
});
|
||||
return `data: ${JSON.stringify(baseChunk)}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Text Delta
|
||||
if (
|
||||
eventType === "response.output_text.delta" ||
|
||||
|
||||
@@ -83,10 +83,65 @@ server.listen(TEST_PORT, "127.0.0.1", async () => {
|
||||
assert.equal(effortRes.status, 200, `Muse Spark with reasoning_effort should be 200 (was ${effortRes.status})`);
|
||||
const effortJson = await effortRes.json();
|
||||
const effortReply = effortJson.choices?.[0]?.message?.content?.trim();
|
||||
const effortReasoning = effortJson.choices?.[0]?.message?.reasoning_content;
|
||||
const effortDuration = ((Date.now() - effortStart) / 1000).toFixed(2);
|
||||
console.log(`✔ Muse Spark with reasoning_effort='high' responded in ${effortDuration}s:`);
|
||||
console.log(` Reasoning: "${effortReasoning ? effortReasoning.slice(0, 80) + '...' : '(none)'}"`);
|
||||
console.log(` Output: "${effortReply}"`);
|
||||
assert.ok(effortReply && /yes/i.test(effortReply), "Muse Spark should answer yes for prime 97");
|
||||
assert.ok(effortReasoning && effortReasoning.length > 0, "Non-streaming message should include reasoning_content");
|
||||
|
||||
// 4. Verify Live Streaming Reasoning Tokens with Muse Spark 1.3 Contributor
|
||||
console.log("\n[4/4] Verifying Live Streaming Reasoning Chunks (Muse Spark 1.3)...");
|
||||
const streamStart = Date.now();
|
||||
const streamRes = await fetch(`${baseUrl}/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model: "muse-spark-1.3-contributor",
|
||||
messages: [{ role: "user", content: "Which is bigger: 9.11 or 9.9?" }],
|
||||
reasoning_effort: "high",
|
||||
stream: true
|
||||
})
|
||||
});
|
||||
|
||||
assert.equal(streamRes.status, 200);
|
||||
const reader = streamRes.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let streamBuf = "";
|
||||
let reasoningTokens = "";
|
||||
let contentTokens = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
streamBuf += decoder.decode(value, { stream: true });
|
||||
const parts = streamBuf.split("\n\n");
|
||||
streamBuf = parts.pop() || "";
|
||||
for (const part of parts) {
|
||||
for (const line of part.split("\n")) {
|
||||
if (line.startsWith("data: ") && !line.includes("[DONE]")) {
|
||||
try {
|
||||
const parsed = JSON.parse(line.slice(6));
|
||||
const delta = parsed.choices?.[0]?.delta;
|
||||
if (delta?.reasoning_content) {
|
||||
reasoningTokens += delta.reasoning_content;
|
||||
}
|
||||
if (delta?.content) {
|
||||
contentTokens += delta.content;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const streamDuration = ((Date.now() - streamStart) / 1000).toFixed(2);
|
||||
console.log(`✔ Stream completed in ${streamDuration}s:`);
|
||||
console.log(` Streamed Reasoning: "${reasoningTokens.slice(0, 100)}..." (${reasoningTokens.length} chars)`);
|
||||
console.log(` Streamed Content: "${contentTokens.slice(0, 100)}..." (${contentTokens.length} chars)`);
|
||||
assert.ok(reasoningTokens.length > 0, "Should receive streaming reasoning_content tokens");
|
||||
assert.ok(contentTokens.length > 0, "Should receive streaming content tokens");
|
||||
|
||||
console.log("\n==================================================");
|
||||
console.log("✔ LIVE VERIFICATION PASSED FOR ALL MODELS & REASONING!");
|
||||
|
||||
@@ -43,7 +43,7 @@ const effortHighReq = {
|
||||
reasoning_effort: "high"
|
||||
};
|
||||
const effortHighPayload = chatCompletionsToResponsesBody(effortHighReq);
|
||||
assert.deepEqual(effortHighPayload.reasoning, { effort: "high" });
|
||||
assert.deepEqual(effortHighPayload.reasoning, { effort: "high", summary: "detailed" });
|
||||
|
||||
const effortXHighReq = {
|
||||
model: "muse-spark-1.3-contributor",
|
||||
@@ -51,7 +51,7 @@ const effortXHighReq = {
|
||||
reasoning_effort: "xhigh"
|
||||
};
|
||||
const effortXHighPayload = chatCompletionsToResponsesBody(effortXHighReq);
|
||||
assert.deepEqual(effortXHighPayload.reasoning, { effort: "xhigh" });
|
||||
assert.deepEqual(effortXHighPayload.reasoning, { effort: "xhigh", summary: "detailed" });
|
||||
|
||||
const effortNoneReq = {
|
||||
model: "muse-spark-1.3-contributor",
|
||||
@@ -67,13 +67,17 @@ const effortNestedReq = {
|
||||
reasoning: { effort: "low" }
|
||||
};
|
||||
const effortNestedPayload = chatCompletionsToResponsesBody(effortNestedReq);
|
||||
assert.deepEqual(effortNestedPayload.reasoning, { effort: "low" });
|
||||
assert.deepEqual(effortNestedPayload.reasoning, { effort: "low", summary: "detailed" });
|
||||
|
||||
// 3. Responses API non-streaming JSON -> Chat Completions JSON
|
||||
const rawResponsesResp = {
|
||||
id: "resp_1234567890",
|
||||
model: "muse-spark-1.3-contributor",
|
||||
output: [
|
||||
{
|
||||
type: "reasoning",
|
||||
summary: [{ type: "summary_text", text: "Reasoning step 1" }, { type: "summary_text", text: "Reasoning step 2" }]
|
||||
},
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
@@ -90,6 +94,7 @@ const rawResponsesResp = {
|
||||
const chatResp = responsesToChatCompletionsResponse(rawResponsesResp, "muse-spark-1.3-contributor");
|
||||
assert.equal(chatResp.id, "chatcmpl_1234567890");
|
||||
assert.equal(chatResp.choices[0].message.content, "Hello from Responses API!");
|
||||
assert.equal(chatResp.choices[0].message.reasoning_content, "Reasoning step 1\n\nReasoning step 2");
|
||||
assert.equal(chatResp.choices[0].finish_reason, "stop");
|
||||
assert.equal(chatResp.usage.prompt_tokens, 15);
|
||||
assert.equal(chatResp.usage.completion_tokens, 8);
|
||||
@@ -97,6 +102,15 @@ assert.equal(chatResp.usage.total_tokens, 23);
|
||||
|
||||
// 4. SSE translation
|
||||
const state = { id: "", model: "muse-spark-1.3-contributor" };
|
||||
|
||||
// 4a. Reasoning summary delta SSE
|
||||
const reasoningChunkRaw = `event: response.reasoning_summary_text.delta\ndata: {"type":"response.reasoning_summary_text.delta","delta":"Thinking through the problem..."}\n\n`;
|
||||
const reasoningChunkOpenAi = translateResponsesEventToOpenAiChunk(reasoningChunkRaw, state);
|
||||
assert.ok(reasoningChunkOpenAi.startsWith("data: "));
|
||||
const parsedReasoningChunk = JSON.parse(reasoningChunkOpenAi.replace("data: ", "").trim());
|
||||
assert.equal(parsedReasoningChunk.choices[0].delta.reasoning_content, "Thinking through the problem...");
|
||||
|
||||
// 4b. Text delta SSE
|
||||
const textChunkRaw = `event: response.output_text.delta\ndata: {"type":"response.output_text.delta","delta":"Hello "}\n\n`;
|
||||
const textChunkOpenAi = translateResponsesEventToOpenAiChunk(textChunkRaw, state);
|
||||
assert.ok(textChunkOpenAi.startsWith("data: "));
|
||||
|
||||
Reference in new issue
Block a user