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 { randomBytes } from "node:crypto";
|
||||||
import { createServer, request as httpRequest } from "node:http";
|
import { createServer, request as httpRequest } from "node:http";
|
||||||
import { request as httpsRequest } from "node:https";
|
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 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);
|
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)
|
headers: providerRequestHeaders(clientReq.headers, target, trace, startSpanId)
|
||||||
}, (upstreamRes) => {
|
}, (upstreamRes) => {
|
||||||
statusCode = upstreamRes.statusCode || 502;
|
statusCode = upstreamRes.statusCode || 502;
|
||||||
|
const filterReasoning = shouldFilterOpenAIReasoning(route, upstreamRes.headers);
|
||||||
clientRes.writeHead(statusCode, {
|
clientRes.writeHead(statusCode, {
|
||||||
...providerResponseHeaders(upstreamRes.headers),
|
...providerResponseHeaders(upstreamRes.headers),
|
||||||
traceparent: traceparent(trace.traceId, startSpanId),
|
traceparent: traceparent(trace.traceId, startSpanId),
|
||||||
"x-hwlab-otel-trace-id": trace.traceId
|
"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("end", settle);
|
||||||
upstreamRes.on("close", settle);
|
upstreamRes.on("close", settle);
|
||||||
upstreamRes.on("error", (error) => {
|
upstreamRes.on("error", (error) => {
|
||||||
@@ -183,6 +186,76 @@ function providerRoute(pathname) {
|
|||||||
return `${publicBasePath}${stripped.startsWith("/") ? stripped : `/${stripped}`}`;
|
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) {
|
function providerRequestHeaders(headers, target, trace, parentSpanId) {
|
||||||
const result = {};
|
const result = {};
|
||||||
for (const [name, value] of Object.entries(headers)) {
|
for (const [name, value] of Object.entries(headers)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user