mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
290 lines
7.7 KiB
TypeScript
290 lines
7.7 KiB
TypeScript
import crypto from "crypto";
|
|
import { spawn } from "child_process";
|
|
import {
|
|
BaseExecutor,
|
|
mergeUpstreamExtraHeaders,
|
|
type ExecuteInput,
|
|
type ProviderCredentials,
|
|
} from "./base.ts";
|
|
import { PROVIDERS } from "../config/constants.ts";
|
|
import {
|
|
buildQoderChunk,
|
|
buildQoderCompletionPayload,
|
|
buildQoderPrompt,
|
|
createQoderErrorResponse,
|
|
extractTextFromQoderEnvelope,
|
|
getQoderCliCommand,
|
|
getQoderCliWorkspace,
|
|
mapQoderModelToLevel,
|
|
parseQoderCliFailure,
|
|
runQoderCliCommand,
|
|
} from "../services/qoderCli.ts";
|
|
|
|
function getPat(credentials: ProviderCredentials): string {
|
|
if (typeof credentials.apiKey === "string" && credentials.apiKey.trim()) {
|
|
return credentials.apiKey.trim();
|
|
}
|
|
return "";
|
|
}
|
|
|
|
export class QoderExecutor extends BaseExecutor {
|
|
constructor() {
|
|
super("qoder", PROVIDERS.qoder);
|
|
}
|
|
|
|
buildHeaders(_credentials: ProviderCredentials, stream = true): Record<string, string> {
|
|
return {
|
|
"Content-Type": "application/json",
|
|
...(stream ? { Accept: "text/event-stream" } : {}),
|
|
};
|
|
}
|
|
|
|
async execute({ model, body, stream, credentials, signal, upstreamExtraHeaders }: ExecuteInput) {
|
|
const headers = this.buildHeaders(credentials, stream);
|
|
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
|
|
|
|
const pat = getPat(credentials);
|
|
if (!pat) {
|
|
return {
|
|
response: createQoderErrorResponse({
|
|
status: 400,
|
|
message:
|
|
"Qoder Personal Access Token is required. Connect Qoder with a PAT via qodercli transport.",
|
|
code: "pat_required",
|
|
}),
|
|
url: "qodercli://local",
|
|
headers,
|
|
transformedBody: body,
|
|
};
|
|
}
|
|
|
|
const prompt = buildQoderPrompt(body);
|
|
const workspace = getQoderCliWorkspace();
|
|
const command = getQoderCliCommand();
|
|
|
|
if (!stream) {
|
|
const result = await runQoderCliCommand({
|
|
token: pat,
|
|
prompt,
|
|
stream: false,
|
|
model,
|
|
workspace,
|
|
command,
|
|
signal,
|
|
});
|
|
|
|
if (!result.ok) {
|
|
const failure = parseQoderCliFailure(result.stderr || result.error || "", result.stdout);
|
|
return {
|
|
response: createQoderErrorResponse(failure),
|
|
url: "qodercli://local",
|
|
headers,
|
|
transformedBody: body,
|
|
};
|
|
}
|
|
|
|
let assistantText = result.stdout.trim();
|
|
try {
|
|
const parsed = JSON.parse(assistantText);
|
|
assistantText = extractTextFromQoderEnvelope(parsed) || assistantText;
|
|
} catch {
|
|
// Fall back to raw stdout if the CLI printed plain text.
|
|
}
|
|
|
|
const payload = buildQoderCompletionPayload({
|
|
model,
|
|
text: assistantText,
|
|
});
|
|
|
|
return {
|
|
response: new Response(JSON.stringify(payload), {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
}),
|
|
url: "qodercli://local",
|
|
headers,
|
|
transformedBody: body,
|
|
};
|
|
}
|
|
|
|
const qoderStream = new ReadableStream({
|
|
start: async (controller) => {
|
|
const encoder = new TextEncoder();
|
|
const created = Math.floor(Date.now() / 1000);
|
|
const responseId = `chatcmpl-${crypto.randomUUID()}`;
|
|
const responseModel = model || "qoder-rome-30ba3b";
|
|
const cliCommand = command;
|
|
const args = [
|
|
"-q",
|
|
"-p",
|
|
prompt,
|
|
"--max-turns",
|
|
"1",
|
|
"--workspace",
|
|
workspace,
|
|
"--output-format",
|
|
"stream-json",
|
|
];
|
|
const level = mapQoderModelToLevel(responseModel);
|
|
if (level) {
|
|
args.push("--model", level);
|
|
}
|
|
|
|
const child = spawn(cliCommand, args, {
|
|
env: {
|
|
...process.env,
|
|
QODER_PERSONAL_ACCESS_TOKEN: pat,
|
|
},
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
...(process.platform === "win32" ? { shell: true } : {}),
|
|
});
|
|
|
|
let stdoutBuffer = "";
|
|
let stderrBuffer = "";
|
|
let emittedText = "";
|
|
let roleSent = false;
|
|
let finished = false;
|
|
|
|
const emitSse = (payload: unknown) => {
|
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(payload)}\n\n`));
|
|
};
|
|
|
|
const finish = () => {
|
|
if (finished) return;
|
|
finished = true;
|
|
emitSse(
|
|
buildQoderChunk({
|
|
id: responseId,
|
|
model: responseModel,
|
|
created,
|
|
delta: {},
|
|
finishReason: "stop",
|
|
})
|
|
);
|
|
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
controller.close();
|
|
};
|
|
|
|
const abortChild = () => {
|
|
try {
|
|
child.kill("SIGTERM");
|
|
} catch {}
|
|
};
|
|
|
|
if (signal?.aborted) {
|
|
abortChild();
|
|
controller.error(new Error("aborted"));
|
|
return;
|
|
}
|
|
|
|
signal?.addEventListener?.(
|
|
"abort",
|
|
() => {
|
|
abortChild();
|
|
controller.error(new Error("aborted"));
|
|
},
|
|
{ once: true }
|
|
);
|
|
|
|
const emitDelta = (deltaText: string) => {
|
|
if (!deltaText) return;
|
|
const delta = roleSent
|
|
? { content: deltaText }
|
|
: { role: "assistant", content: deltaText };
|
|
roleSent = true;
|
|
emitSse(
|
|
buildQoderChunk({
|
|
id: responseId,
|
|
model: responseModel,
|
|
created,
|
|
delta,
|
|
})
|
|
);
|
|
};
|
|
|
|
const drainStdout = () => {
|
|
let newlineIndex = stdoutBuffer.indexOf("\n");
|
|
while (newlineIndex >= 0) {
|
|
const rawLine = stdoutBuffer.slice(0, newlineIndex).trim();
|
|
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
|
|
newlineIndex = stdoutBuffer.indexOf("\n");
|
|
|
|
if (!rawLine) continue;
|
|
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(rawLine);
|
|
} catch {
|
|
continue;
|
|
}
|
|
|
|
const nextText = extractTextFromQoderEnvelope(parsed);
|
|
if (nextText) {
|
|
const delta = nextText.startsWith(emittedText)
|
|
? nextText.slice(emittedText.length)
|
|
: nextText;
|
|
emittedText += delta;
|
|
emitDelta(delta);
|
|
}
|
|
|
|
const parsedRecord =
|
|
parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
? (parsed as Record<string, unknown>)
|
|
: {};
|
|
if (parsedRecord.type === "result" && parsedRecord.done === true) {
|
|
finish();
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
|
|
child.stdout.on("data", (chunk) => {
|
|
stdoutBuffer += chunk;
|
|
drainStdout();
|
|
});
|
|
|
|
child.stderr.on("data", (chunk) => {
|
|
stderrBuffer += chunk;
|
|
});
|
|
|
|
child.on("error", (error) => {
|
|
if (finished) return;
|
|
controller.error(error);
|
|
});
|
|
|
|
child.on("close", (code) => {
|
|
if (finished) return;
|
|
drainStdout();
|
|
if (code !== 0) {
|
|
const failure = parseQoderCliFailure(stderrBuffer, stdoutBuffer);
|
|
controller.error(new Error(failure.message));
|
|
return;
|
|
}
|
|
finish();
|
|
});
|
|
},
|
|
});
|
|
|
|
return {
|
|
response: new Response(qoderStream, {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
"Cache-Control": "no-cache, no-transform",
|
|
Connection: "keep-alive",
|
|
},
|
|
}),
|
|
url: "qodercli://local",
|
|
headers,
|
|
transformedBody: body,
|
|
};
|
|
}
|
|
}
|
|
|
|
export default QoderExecutor;
|