mirror of
https://github.com/Jeuners/agenttwo.git
synced 2026-09-09 15:02:31 +02:00
116 lines
3.2 KiB
TypeScript
116 lines
3.2 KiB
TypeScript
export interface OpenRouterOptions {
|
|
model: string;
|
|
temperature: number;
|
|
numPredict: number;
|
|
}
|
|
|
|
export interface StreamCallbacks {
|
|
onThinking(text: string): void;
|
|
onToken(text: string): void;
|
|
onDone(): void;
|
|
}
|
|
|
|
export function getOpenRouterKey(): string | undefined {
|
|
return process.env.OPENROUTER_API_KEY;
|
|
}
|
|
|
|
export async function listOpenRouterModels(): Promise<
|
|
{ id: string; name: string; contextLength: number; promptPrice: number }[]
|
|
> {
|
|
const res = await fetch("https://openrouter.ai/api/v1/models");
|
|
if (!res.ok) throw new Error(`OpenRouter HTTP ${res.status}`);
|
|
const data = (await res.json()) as {
|
|
data: {
|
|
id: string;
|
|
name: string;
|
|
context_length: number;
|
|
pricing: { prompt: string };
|
|
}[];
|
|
};
|
|
return data.data
|
|
.map((m) => ({
|
|
id: m.id,
|
|
name: m.name,
|
|
contextLength: m.context_length,
|
|
promptPrice: Number(m.pricing?.prompt ?? 0) * 1_000_000,
|
|
}))
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
|
|
export async function streamOpenRouter(
|
|
history: { role: string; content: string }[],
|
|
systemPrompt: string | undefined,
|
|
opts: OpenRouterOptions,
|
|
apiKey: string,
|
|
cb: StreamCallbacks,
|
|
signal: AbortSignal,
|
|
): Promise<void> {
|
|
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${apiKey}`,
|
|
"Content-Type": "application/json",
|
|
"HTTP-Referer": "http://localhost:5173",
|
|
"X-Title": "oxagenttwo",
|
|
},
|
|
body: JSON.stringify({
|
|
model: opts.model,
|
|
stream: true,
|
|
messages: [
|
|
...(systemPrompt ? [{ role: "system", content: systemPrompt }] : []),
|
|
...history,
|
|
],
|
|
temperature: opts.temperature,
|
|
max_tokens: opts.numPredict,
|
|
}),
|
|
signal,
|
|
});
|
|
|
|
if (!res.ok || !res.body) {
|
|
const text = await res.text().catch(() => res.statusText);
|
|
throw new Error(`OpenRouter HTTP ${res.status}: ${text.slice(0, 300)}`);
|
|
}
|
|
|
|
const reader = res.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = "";
|
|
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
let nlIndex: number;
|
|
while ((nlIndex = buffer.indexOf("\n")) !== -1) {
|
|
const line = buffer.slice(0, nlIndex).trim();
|
|
buffer = buffer.slice(nlIndex + 1);
|
|
if (!line.startsWith("data:")) continue;
|
|
const payload = line.slice(5).trim();
|
|
if (payload === "[DONE]") {
|
|
cb.onDone();
|
|
return;
|
|
}
|
|
let chunk: {
|
|
choices?: {
|
|
delta?: { content?: string; reasoning?: string };
|
|
finish_reason?: string | null;
|
|
}[];
|
|
error?: { message?: string };
|
|
};
|
|
try {
|
|
chunk = JSON.parse(payload);
|
|
} catch {
|
|
continue;
|
|
}
|
|
if (chunk.error) throw new Error(chunk.error.message ?? "OpenRouter error");
|
|
const delta = chunk.choices?.[0]?.delta;
|
|
if (delta?.reasoning) cb.onThinking(delta.reasoning);
|
|
if (delta?.content) cb.onToken(delta.content);
|
|
if (chunk.choices?.[0]?.finish_reason && !delta?.content) {
|
|
cb.onDone();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
cb.onDone();
|
|
}
|