Merge pull request #2288 from pikasTech/issue-2274-opencode-strip-reasoning

fix: strip OpenCode reasoning stream deltas
This commit is contained in:
Lyon
2026-06-30 13:25:21 +08:00
committed by GitHub
@@ -2,6 +2,7 @@
import { randomBytes } from "node:crypto";
import { createServer, request as httpRequest } from "node:http";
import { request as httpsRequest } from "node:https";
import { Transform } from "node:stream";
const serviceId = process.env.OTEL_SERVICE_NAME || process.env.HWLAB_SERVICE_ID || "opencode-provider-proxy";
const port = parsePositiveInteger(process.env.PORT || process.env.HWLAB_OPENCODE_PROVIDER_PROXY_PORT, 4097);
@@ -110,12 +111,14 @@ async function proxyProviderRequest(clientReq, clientRes, url) {
headers: providerRequestHeaders(clientReq.headers, target, trace, startSpanId)
}, (upstreamRes) => {
statusCode = upstreamRes.statusCode || 502;
const filterReasoning = shouldFilterOpenAIReasoning(route, upstreamRes.headers);
clientRes.writeHead(statusCode, {
...providerResponseHeaders(upstreamRes.headers),
traceparent: traceparent(trace.traceId, startSpanId),
"x-hwlab-otel-trace-id": trace.traceId
});
upstreamRes.pipe(clientRes);
const responseStream = filterReasoning ? upstreamRes.pipe(openAIReasoningFilterStream()) : upstreamRes;
responseStream.pipe(clientRes);
upstreamRes.on("end", settle);
upstreamRes.on("close", settle);
upstreamRes.on("error", (error) => {
@@ -183,6 +186,76 @@ function providerRoute(pathname) {
return `${publicBasePath}${stripped.startsWith("/") ? stripped : `/${stripped}`}`;
}
function shouldFilterOpenAIReasoning(route, headers) {
return route === `${publicBasePath}/chat/completions` && /^text\/event-stream(?:\s*;|$)/iu.test(firstHeaderValue(headers?.["content-type"]));
}
function openAIReasoningFilterStream() {
let pending = "";
return new Transform({
transform(chunk, _encoding, callback) {
pending += chunk.toString("utf8");
let newlineIndex = pending.indexOf("\n");
while (newlineIndex >= 0) {
const line = pending.slice(0, newlineIndex + 1);
pending = pending.slice(newlineIndex + 1);
const next = filterOpenAISseLine(line);
if (next) this.push(next);
newlineIndex = pending.indexOf("\n");
}
callback();
},
flush(callback) {
if (pending) {
const next = filterOpenAISseLine(pending);
if (next) this.push(next);
}
callback();
}
});
}
function filterOpenAISseLine(line) {
const match = line.match(/^(data:\s*)(.*?)(\r?\n)?$/u);
if (!match) return line;
const [, prefix, payload, lineEnding = ""] = match;
const text = payload.trim();
if (!text || text === "[DONE]") return line;
try {
const parsed = JSON.parse(text);
const choices = Array.isArray(parsed.choices) ? parsed.choices.map(filterOpenAIChoiceReasoning).filter(Boolean) : parsed.choices;
if (Array.isArray(parsed.choices)) {
if (choices.length === 0 && parsed.usage === null) return "";
parsed.choices = choices;
}
return `${prefix}${JSON.stringify(parsed)}${lineEnding}`;
} catch {
return line;
}
}
function filterOpenAIChoiceReasoning(choice) {
if (!choice || typeof choice !== "object") return choice;
const next = { ...choice };
if (next.delta && typeof next.delta === "object") {
const delta = { ...next.delta };
delete delta.reasoning_content;
delete delta.reasoning;
delete delta.reasoning_details;
if (delta.content === null) delete delta.content;
next.delta = delta;
}
if (next.message && typeof next.message === "object") {
next.message = { ...next.message };
delete next.message.reasoning_content;
delete next.message.reasoning;
delete next.message.reasoning_details;
}
const deltaEmpty = next.delta && typeof next.delta === "object" && Object.keys(next.delta).length === 0;
if (deltaEmpty && next.finish_reason == null && next.logprobs == null) return null;
return next;
}
function providerRequestHeaders(headers, target, trace, parentSpanId) {
const result = {};
for (const [name, value] of Object.entries(headers)) {