openclaw-freerouter 2.0.2 → 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/README.md CHANGED
@@ -186,7 +186,7 @@ After install, add to your `openclaw.json`:
186
186
  // 3. Plugin config
187
187
  "plugins": {
188
188
  "entries": {
189
- "freerouter": {
189
+ "openclaw-freerouter": {
190
190
  "enabled": true,
191
191
  "config": {
192
192
  "port": 18801,
@@ -234,7 +234,7 @@ Then restart: `openclaw gateway restart`
234
234
  If port 18801 is in use, change it:
235
235
 
236
236
  ```json5
237
- { "plugins": { "entries": { "freerouter": { "config": { "port": 18802 } } } } }
237
+ { "plugins": { "entries": { "openclaw-freerouter": { "config": { "port": 18802 } } } } }
238
238
  ```
239
239
 
240
240
  Set `"port": 0` to disable the HTTP proxy entirely.
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "freerouter",
2
+ "id": "openclaw-freerouter",
3
3
  "name": "FreeRouter",
4
4
  "version": "2.0.0",
5
5
  "description": "Smart LLM router — classify requests and route to the best model using your own API keys. 14-dimension weighted scoring, <1ms classification, configurable tiers.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-freerouter",
3
- "version": "2.0.2",
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/cli.ts CHANGED
@@ -16,7 +16,7 @@ import { buildPricingMap } from "./models.js";
16
16
  export function registerCli(api: any) {
17
17
  const logger = api.logger ?? console;
18
18
 
19
- const getPluginConfig = () => api.config?.plugins?.entries?.freerouter?.config ?? {};
19
+ const getPluginConfig = () => api.config?.plugins?.entries?.["openclaw-freerouter"]?.config ?? {};
20
20
 
21
21
  api.registerCli(
22
22
  ({ program }: any) => {
@@ -162,8 +162,8 @@ export function registerCli(api: any) {
162
162
  enabled: { models: ["claude-sonnet-4-5"], budget: 4096 },
163
163
  },
164
164
  }, null, 2));
165
- console.log("\n Copy this into plugins.entries.freerouter.config in openclaw.json");
166
- console.log(" Or run: openclaw config set plugins.entries.freerouter.config '{...}'");
165
+ console.log('\n Copy this into plugins.entries["openclaw-freerouter"].config in openclaw.json');
166
+ console.log(' Or run: openclaw config set plugins.entries["openclaw-freerouter"].config \'{...}\'');
167
167
  console.log();
168
168
  });
169
169
 
@@ -335,7 +335,7 @@ export function registerCli(api: any) {
335
335
 
336
336
  Step 1: Add this to your openclaw.json under "plugins.entries":
337
337
 
338
- "freerouter": {
338
+ "openclaw-freerouter": {
339
339
  "enabled": true,
340
340
  "config": ${JSON.stringify(pluginConfig, null, 6).split("\n").map((l, i) => i === 0 ? l : " " + l).join("\n")}
341
341
  }
package/src/index.ts CHANGED
@@ -22,7 +22,7 @@ export const id = "freerouter";
22
22
  export default function register(api: any) {
23
23
  const logger = api.logger ?? console;
24
24
  const getPluginConfig = () => {
25
- const cfg = api.config?.plugins?.entries?.freerouter?.config ?? {};
25
+ const cfg = api.config?.plugins?.entries?.["openclaw-freerouter"]?.config ?? {};
26
26
  return cfg;
27
27
  };
28
28
 
@@ -55,7 +55,7 @@ export default function register(api: any) {
55
55
  server.on("error", (err: any) => {
56
56
  if (err.code === "EADDRINUSE") {
57
57
  logger.error(`[freerouter] Port ${port} is already in use. Run: openclaw freerouter doctor`);
58
- logger.error(`[freerouter] To change port: set plugins.entries.freerouter.config.port in openclaw.json`);
58
+ logger.error(`[freerouter] To change port: set plugins.entries["openclaw-freerouter"].config.port in openclaw.json`);
59
59
  } else {
60
60
  logger.error(`[freerouter] Server error: ${err.message}`);
61
61
  }
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(filePath, "utf-8"));
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
- if (ocProviders?.[provider]) {
104
- const p = ocProviders[provider];
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
- if (req.max_tokens) body.max_tokens = req.max_tokens;
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
- await forwarder.forward(chatReq, modelToTry, tier, thinkingMode, res, stream);
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
- res.writeHead(200, { "Content-Type": "application/json" });
716
- res.end(JSON.stringify({ status: "ok", version: "2.0.0", uptime: process.uptime(), stats }));
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) {