2026-08-18 23:24:27 -04:00
|
|
|
// shre-router client. POST /v1/chat with stream:false.
|
|
|
|
|
// Real model answer lives in .message.content; top-level .content can carry an
|
|
|
|
|
// upstream-down apology — always prefer .message.content.
|
|
|
|
|
|
|
|
|
|
export async function chat({ routerUrl, model, messages, agentId, sessionId, tenantId, timeoutMs }) {
|
|
|
|
|
const res = await fetch(`${routerUrl.replace(/\/$/, '')}/v1/chat`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
2026-08-18 23:41:09 -04:00
|
|
|
// tools:false — reviews need pure model reasoning; it also keeps the
|
|
|
|
|
// router's tool/skill fast-paths (e.g. current-info briefing) from
|
|
|
|
|
// hijacking prompts whose diffs mention GitHub/news-like terms.
|
|
|
|
|
body: JSON.stringify({ model, messages, agentId, sessionId, tenantId, stream: false, tools: false }),
|
2026-08-18 23:24:27 -04:00
|
|
|
signal: AbortSignal.timeout(timeoutMs)
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const text = await res.text().catch(() => '');
|
|
|
|
|
throw new Error(`router -> ${res.status}: ${text.slice(0, 300)}`);
|
|
|
|
|
}
|
|
|
|
|
const data = await res.json();
|
2026-08-18 23:35:12 -04:00
|
|
|
// Response shape varies by upstream: prefer .message.content, then
|
|
|
|
|
// OpenAI-style choices, then top-level .content (which can carry an
|
|
|
|
|
// upstream-down apology — hence last).
|
|
|
|
|
const content = data?.message?.content
|
|
|
|
|
?? data?.choices?.[0]?.message?.content
|
|
|
|
|
?? data?.content;
|
2026-08-18 23:24:27 -04:00
|
|
|
if (typeof content !== 'string' || !content.trim()) {
|
|
|
|
|
throw new Error('router returned empty content');
|
|
|
|
|
}
|
2026-08-18 23:35:12 -04:00
|
|
|
return { content, servedModel: data?._shre?.model ?? null };
|
2026-08-18 23:24:27 -04:00
|
|
|
}
|