feat(bridge): route llm.ts via self-hosted LiteLLM gateway
OpenRouter credits ran dry and silently killed classifyAgent (task routing). Non-anthropic slugs now go to llm.manohargupta.com (own MiniMax/Anthropic keys). New env: LLM_GATEWAY_URL, LLM_GATEWAY_KEY. Default router model: minimax-3.
This commit is contained in:
parent
c3cd924cdd
commit
61e386f7fe
1 changed files with 22 additions and 22 deletions
|
|
@ -6,22 +6,24 @@
|
||||||
* generateProjectTitle(text) → 3-7 word project title
|
* generateProjectTitle(text) → 3-7 word project title
|
||||||
* generateProjectGoal(text) → one-line success criterion
|
* generateProjectGoal(text) → one-line success criterion
|
||||||
*
|
*
|
||||||
* Configured via env vars (already declared in bridge/.env):
|
* Configured via env vars (declared in bridge/.env):
|
||||||
* TIGER_ROUTER_MODEL Model slug for ALL router calls.
|
* TIGER_ROUTER_MODEL Model slug for ALL router calls.
|
||||||
* Examples:
|
* Examples:
|
||||||
* "anthropic/claude-haiku-4-5" → Anthropic API direct
|
* "anthropic/claude-haiku-4-5" → Anthropic API direct
|
||||||
* "minimax/MiniMax-M2.7" → OpenRouter
|
* "minimax-3" → self-hosted LiteLLM gateway
|
||||||
* "openrouter/auto" → OpenRouter (meta-router)
|
* Default if unset: "minimax-3" (gateway).
|
||||||
* Default if unset: "anthropic/claude-haiku-4-5".
|
|
||||||
* ANTHROPIC_API_KEY Required when ROUTER_MODEL has "anthropic/" prefix.
|
* ANTHROPIC_API_KEY Required when ROUTER_MODEL has "anthropic/" prefix.
|
||||||
* OPENROUTER_API_KEY Required for everything else.
|
* LLM_GATEWAY_URL Self-hosted gateway base URL.
|
||||||
|
* Default: https://llm.manohargupta.com/v1
|
||||||
|
* LLM_GATEWAY_KEY Bearer key for the gateway (LiteLLM master/virtual key).
|
||||||
*
|
*
|
||||||
* Routing rule (intentionally simple):
|
* Routing rule (intentionally simple):
|
||||||
* slug startsWith "anthropic/" → Anthropic API, model = slug minus "anthropic/"
|
* slug startsWith "anthropic/" → Anthropic API, model = slug minus "anthropic/"
|
||||||
* anything else → OpenRouter, model = slug verbatim
|
* anything else → LiteLLM gateway, model = slug verbatim
|
||||||
*
|
*
|
||||||
* Note: "openrouter/auto" is OR's literal model ID, so we DON'T strip it.
|
* OpenRouter was removed 2026-06-10: its credits ran dry and silently took
|
||||||
* This is why the rule only special-cases "anthropic/".
|
* classifyAgent down with it. The gateway runs on Manohar's own MiniMax /
|
||||||
|
* Anthropic keys, so there is no third-party balance to surprise us.
|
||||||
*
|
*
|
||||||
* Failure mode (the most important property):
|
* Failure mode (the most important property):
|
||||||
* Every public helper catches errors internally. Callers never see exceptions
|
* Every public helper catches errors internally. Callers never see exceptions
|
||||||
|
|
@ -34,9 +36,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ─── Configuration ─────────────────────────────────────────────────────────
|
// ─── Configuration ─────────────────────────────────────────────────────────
|
||||||
const ROUTER_MODEL = process.env.TIGER_ROUTER_MODEL || "anthropic/claude-haiku-4-5";
|
const ROUTER_MODEL = process.env.TIGER_ROUTER_MODEL || "minimax-3";
|
||||||
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || "";
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || "";
|
||||||
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY || "";
|
const LLM_GATEWAY_URL = (process.env.LLM_GATEWAY_URL || "https://llm.manohargupta.com/v1").replace(/\/$/, "");
|
||||||
|
const LLM_GATEWAY_KEY = process.env.LLM_GATEWAY_KEY || "";
|
||||||
const ANTHROPIC_VERSION = "2023-06-01";
|
const ANTHROPIC_VERSION = "2023-06-01";
|
||||||
|
|
||||||
// Curated list of valid agent IDs. Used to validate classifier output.
|
// Curated list of valid agent IDs. Used to validate classifier output.
|
||||||
|
|
@ -45,7 +48,7 @@ export type AgentId = (typeof AGENT_IDS)[number];
|
||||||
|
|
||||||
// ─── Internal: provider resolution ──────────────────────────────────────────
|
// ─── Internal: provider resolution ──────────────────────────────────────────
|
||||||
interface ResolvedModel {
|
interface ResolvedModel {
|
||||||
provider: "anthropic" | "openrouter";
|
provider: "anthropic" | "gateway";
|
||||||
model: string;
|
model: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +60,7 @@ function resolveModel(slug: string): ResolvedModel {
|
||||||
if (slug.startsWith("anthropic/")) {
|
if (slug.startsWith("anthropic/")) {
|
||||||
return { provider: "anthropic", model: slug.slice("anthropic/".length) };
|
return { provider: "anthropic", model: slug.slice("anthropic/".length) };
|
||||||
}
|
}
|
||||||
return { provider: "openrouter", model: slug };
|
return { provider: "gateway", model: slug };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Internal: low-level LLM call ───────────────────────────────────────────
|
// ─── Internal: low-level LLM call ───────────────────────────────────────────
|
||||||
|
|
@ -107,18 +110,15 @@ async function callLLM(
|
||||||
return text.trim();
|
return text.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenRouter (catch-all for everything except "anthropic/")
|
// Self-hosted LiteLLM gateway (catch-all for everything except "anthropic/")
|
||||||
if (!OPENROUTER_API_KEY) {
|
if (!LLM_GATEWAY_KEY) {
|
||||||
throw new Error("OPENROUTER_API_KEY not set");
|
throw new Error("LLM_GATEWAY_KEY not set");
|
||||||
}
|
}
|
||||||
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
const res = await fetch(`${LLM_GATEWAY_URL}/chat/completions`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${OPENROUTER_API_KEY}`,
|
Authorization: `Bearer ${LLM_GATEWAY_KEY}`,
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
// OR recommends these for observability/ranking — harmless if ignored.
|
|
||||||
"HTTP-Referer": "https://agent.manohargupta.com",
|
|
||||||
"X-Title": "Tiger Bridge Router",
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model,
|
model,
|
||||||
|
|
@ -131,13 +131,13 @@ async function callLLM(
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errBody = await res.text().catch(() => "<no body>");
|
const errBody = await res.text().catch(() => "<no body>");
|
||||||
throw new Error(`OpenRouter API ${res.status}: ${errBody.slice(0, 200)}`);
|
throw new Error(`LLM gateway ${res.status}: ${errBody.slice(0, 200)}`);
|
||||||
}
|
}
|
||||||
const data = (await res.json()) as {
|
const data = (await res.json()) as {
|
||||||
choices?: Array<{ message?: { content?: string } }>;
|
choices?: Array<{ message?: { content?: string } }>;
|
||||||
};
|
};
|
||||||
const text = data.choices?.[0]?.message?.content;
|
const text = data.choices?.[0]?.message?.content;
|
||||||
if (!text) throw new Error("OpenRouter returned no message content");
|
if (!text) throw new Error("LLM gateway returned no message content");
|
||||||
return text.trim();
|
return text.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue