Merge pull request #2288 from pikasTech/issue-2274-opencode-strip-reasoning
fix: strip OpenCode reasoning stream deltas
This commit is contained in:
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user