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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-freerouter",
3
- "version": "2.1.0",
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(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) {