openclaw-freerouter 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/provider.ts +57 -5
- package/src/service.ts +51 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-freerouter",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Smart LLM router plugin for OpenClaw — classify requests and route to the best model using your own API keys. 14-dimension scoring, <1ms classification, per-prompt/session model switching.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"openclaw": {
|
package/src/provider.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { join } from "node:path";
|
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import type { ServerResponse } from "node:http";
|
|
14
14
|
|
|
15
|
+
const AUTH_PROFILES_PATH = join(homedir(), ".openclaw", "agents", "main", "agent", "auth-profiles.json");
|
|
16
|
+
|
|
15
17
|
// ─── Timeout Configuration ───
|
|
16
18
|
const TIER_TIMEOUTS: Record<string, number> = {
|
|
17
19
|
SIMPLE: 30_000,
|
|
@@ -29,6 +31,15 @@ export class TimeoutError extends Error {
|
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
export type PreflightResult = {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
issues: Array<{
|
|
37
|
+
provider: string;
|
|
38
|
+
reason: string;
|
|
39
|
+
model?: string;
|
|
40
|
+
}>;
|
|
41
|
+
};
|
|
42
|
+
|
|
32
43
|
// ─── Provider Config ───
|
|
33
44
|
|
|
34
45
|
type ProviderDef = {
|
|
@@ -58,9 +69,8 @@ type AuthProfiles = {
|
|
|
58
69
|
};
|
|
59
70
|
|
|
60
71
|
function loadAuthProfiles(): Map<string, { token?: string; apiKey?: string }> {
|
|
61
|
-
const filePath = join(homedir(), ".openclaw", "agents", "main", "agent", "auth-profiles.json");
|
|
62
72
|
try {
|
|
63
|
-
const data: AuthProfiles = JSON.parse(readFileSync(
|
|
73
|
+
const data: AuthProfiles = JSON.parse(readFileSync(AUTH_PROFILES_PATH, "utf-8"));
|
|
64
74
|
const map = new Map<string, { token?: string; apiKey?: string }>();
|
|
65
75
|
const lastGood = data.lastGood ?? {};
|
|
66
76
|
|
|
@@ -100,8 +110,9 @@ export function createForwarder(openclawConfig: any, logger: any) {
|
|
|
100
110
|
function getProviderDef(provider: string): ProviderDef | undefined {
|
|
101
111
|
// Check OpenClaw config for custom provider definitions
|
|
102
112
|
const ocProviders = (openclawConfig as any)?.providers;
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
const modelsProviders = (openclawConfig as any)?.models?.providers;
|
|
114
|
+
const p = ocProviders?.[provider] ?? modelsProviders?.[provider];
|
|
115
|
+
if (p) {
|
|
105
116
|
return {
|
|
106
117
|
baseUrl: p.baseUrl ?? DEFAULT_PROVIDERS[provider]?.baseUrl,
|
|
107
118
|
api: p.api === "anthropic-messages" ? "anthropic" : (p.api ?? DEFAULT_PROVIDERS[provider]?.api ?? "openai"),
|
|
@@ -111,6 +122,38 @@ export function createForwarder(openclawConfig: any, logger: any) {
|
|
|
111
122
|
return DEFAULT_PROVIDERS[provider];
|
|
112
123
|
}
|
|
113
124
|
|
|
125
|
+
function preflightModels(modelIds: string[]): PreflightResult {
|
|
126
|
+
const seen = new Set<string>();
|
|
127
|
+
const issues: PreflightResult["issues"] = [];
|
|
128
|
+
|
|
129
|
+
for (const modelId of modelIds) {
|
|
130
|
+
const parsed = parseModelId(modelId);
|
|
131
|
+
if (seen.has(parsed.provider)) continue;
|
|
132
|
+
seen.add(parsed.provider);
|
|
133
|
+
|
|
134
|
+
const providerDef = getProviderDef(parsed.provider);
|
|
135
|
+
if (!providerDef) {
|
|
136
|
+
issues.push({
|
|
137
|
+
provider: parsed.provider,
|
|
138
|
+
reason: `Unsupported provider: ${parsed.provider}`,
|
|
139
|
+
model: modelId,
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const auth = getAuth(parsed.provider);
|
|
145
|
+
if (!auth?.token && !auth?.apiKey) {
|
|
146
|
+
issues.push({
|
|
147
|
+
provider: parsed.provider,
|
|
148
|
+
model: modelId,
|
|
149
|
+
reason: `No API key found for provider \"${parsed.provider}\". Auth store: ${AUTH_PROFILES_PATH}`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { ok: issues.length === 0, issues };
|
|
155
|
+
}
|
|
156
|
+
|
|
114
157
|
function getThinkingConfig(thinkingMode: string, _modelId: string): { type: string; budget_tokens?: number } | undefined {
|
|
115
158
|
if (thinkingMode === "adaptive") return { type: "adaptive" };
|
|
116
159
|
const budgetMatch = thinkingMode.match(/^enabled\((\d+)\)$/);
|
|
@@ -452,7 +495,15 @@ export function createForwarder(openclawConfig: any, logger: any) {
|
|
|
452
495
|
messages: req.messages,
|
|
453
496
|
stream,
|
|
454
497
|
};
|
|
455
|
-
|
|
498
|
+
// GPT-5.x and newer OpenAI models require max_completion_tokens instead of max_tokens
|
|
499
|
+
const isNewOpenAI = provider === "openai" && /^gpt-[5-9]/.test(modelName);
|
|
500
|
+
if (req.max_tokens || req.max_completion_tokens) {
|
|
501
|
+
if (isNewOpenAI) {
|
|
502
|
+
body.max_completion_tokens = req.max_completion_tokens ?? req.max_tokens;
|
|
503
|
+
} else {
|
|
504
|
+
body.max_tokens = req.max_tokens ?? req.max_completion_tokens;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
456
507
|
if (req.temperature !== undefined) body.temperature = req.temperature;
|
|
457
508
|
if (req.top_p !== undefined) body.top_p = req.top_p;
|
|
458
509
|
|
|
@@ -546,6 +597,7 @@ export function createForwarder(openclawConfig: any, logger: any) {
|
|
|
546
597
|
// ─── Main forward function ───
|
|
547
598
|
|
|
548
599
|
return {
|
|
600
|
+
preflight: preflightModels,
|
|
549
601
|
forward: async (chatReq: any, routedModel: string, tier: string, thinkingMode: string, res: ServerResponse, stream: boolean) => {
|
|
550
602
|
const { provider, model } = parseModelId(routedModel);
|
|
551
603
|
const providerDef = getProviderDef(provider);
|
package/src/service.ts
CHANGED
|
@@ -403,6 +403,17 @@ export function createProxyServer(options: ProxyOptions): { server: Server; stat
|
|
|
403
403
|
byModel: {},
|
|
404
404
|
};
|
|
405
405
|
|
|
406
|
+
function preflightHealth() {
|
|
407
|
+
const modelSet = new Set<string>(["freerouter/auto"]);
|
|
408
|
+
for (const tier of Object.values(routingConfig.tiers)) {
|
|
409
|
+
if (tier?.primary) modelSet.add(tier.primary);
|
|
410
|
+
for (const fb of tier?.fallback ?? []) {
|
|
411
|
+
modelSet.add(fb);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return forwarder.preflight(Array.from(modelSet));
|
|
415
|
+
}
|
|
416
|
+
|
|
406
417
|
async function handleChatCompletions(req: IncomingMessage, res: ServerResponse) {
|
|
407
418
|
const bodyStr = await readBody(req);
|
|
408
419
|
let chatReq: any;
|
|
@@ -660,6 +671,22 @@ export function createProxyServer(options: ProxyOptions): { server: Server; stat
|
|
|
660
671
|
}
|
|
661
672
|
}
|
|
662
673
|
|
|
674
|
+
// Preflight check: ensure auth/config for every provider we might hit.
|
|
675
|
+
const preflightModels = [requestedModel, ...modelsToTry];
|
|
676
|
+
const preflight = forwarder.preflight(preflightModels);
|
|
677
|
+
if (!preflight.ok) {
|
|
678
|
+
const issue = preflight.issues[0];
|
|
679
|
+
if (!issue) {
|
|
680
|
+
sendError(res, 503, "Freerouter preflight failed", "auth_error");
|
|
681
|
+
} else {
|
|
682
|
+
const details = preflight.issues
|
|
683
|
+
.map((i) => `${i.provider}: ${i.reason}`)
|
|
684
|
+
.join("; ");
|
|
685
|
+
sendError(res, 503, `Freerouter preflight failed: ${details}`, "auth_error");
|
|
686
|
+
}
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
663
690
|
let lastError = "";
|
|
664
691
|
for (const modelToTry of modelsToTry) {
|
|
665
692
|
try {
|
|
@@ -669,7 +696,15 @@ export function createProxyServer(options: ProxyOptions): { server: Server; stat
|
|
|
669
696
|
}
|
|
670
697
|
|
|
671
698
|
// KEY FIX: Forward with the REAL model name — not "freerouter/X"
|
|
672
|
-
|
|
699
|
+
// Inject model identity into messages so the agent knows which model it's running on
|
|
700
|
+
const injectedMessages = [...chatReq.messages];
|
|
701
|
+
const modelHint = `[FreeRouter] You are running on model: ${modelToTry} | Tier: ${tier} | Thinking: ${thinkingMode}`;
|
|
702
|
+
// Insert as a developer message right after system prompts (before user messages)
|
|
703
|
+
const lastSystemIdx = injectedMessages.reduce((acc: number, m: any, i: number) =>
|
|
704
|
+
(m.role === "system" || m.role === "developer") ? i : acc, -1);
|
|
705
|
+
injectedMessages.splice(lastSystemIdx + 1, 0, { role: "developer", content: modelHint });
|
|
706
|
+
const injectedReq = { ...chatReq, messages: injectedMessages };
|
|
707
|
+
await forwarder.forward(injectedReq, modelToTry, tier, thinkingMode, res, stream);
|
|
673
708
|
return;
|
|
674
709
|
} catch (err: any) {
|
|
675
710
|
lastError = err.message ?? String(err);
|
|
@@ -712,8 +747,21 @@ export function createProxyServer(options: ProxyOptions): { server: Server; stat
|
|
|
712
747
|
}
|
|
713
748
|
|
|
714
749
|
function handleHealth(_req: IncomingMessage, res: ServerResponse) {
|
|
715
|
-
|
|
716
|
-
|
|
750
|
+
const pf = preflightHealth();
|
|
751
|
+
const status = pf.ok ? "ok" : "degraded";
|
|
752
|
+
const payload: Record<string, unknown> = {
|
|
753
|
+
status,
|
|
754
|
+
version: "2.0.0",
|
|
755
|
+
uptime: process.uptime(),
|
|
756
|
+
stats,
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
if (!pf.ok) {
|
|
760
|
+
payload.issues = pf.issues;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
res.writeHead(pf.ok ? 200 : 503, { "Content-Type": "application/json" });
|
|
764
|
+
res.end(JSON.stringify(payload));
|
|
717
765
|
}
|
|
718
766
|
|
|
719
767
|
function handleStats(_req: IncomingMessage, res: ServerResponse) {
|