daedalus-cli 3.7.1 → 3.7.3

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +3 -0
  3. package/dist/commands/dev.d.ts.map +1 -1
  4. package/dist/commands/dev.js +106 -3
  5. package/dist/commands/dev.js.map +1 -1
  6. package/dist/commands/index.js +1 -1
  7. package/dist/commands/index.js.map +1 -1
  8. package/dist/config/index.d.ts +45 -0
  9. package/dist/config/index.d.ts.map +1 -1
  10. package/dist/config/index.js +18 -0
  11. package/dist/config/index.js.map +1 -1
  12. package/dist/config/systemInfo.d.ts.map +1 -1
  13. package/dist/config/systemInfo.js +2 -1
  14. package/dist/config/systemInfo.js.map +1 -1
  15. package/dist/index.js +30 -14
  16. package/dist/index.js.map +1 -1
  17. package/dist/router/health.d.ts +2 -0
  18. package/dist/router/health.d.ts.map +1 -1
  19. package/dist/router/health.js +85 -0
  20. package/dist/router/health.js.map +1 -1
  21. package/dist/router/health.test.js +38 -1
  22. package/dist/router/health.test.js.map +1 -1
  23. package/dist/router/index.d.ts +30 -0
  24. package/dist/router/index.d.ts.map +1 -1
  25. package/dist/router/index.js +168 -13
  26. package/dist/router/index.js.map +1 -1
  27. package/dist/router/index.test.js +180 -0
  28. package/dist/router/index.test.js.map +1 -1
  29. package/dist/router/types.d.ts +5 -0
  30. package/dist/router/types.d.ts.map +1 -1
  31. package/dist/tools/mcp/registry.d.ts.map +1 -1
  32. package/dist/tools/mcp/registry.js +3 -1
  33. package/dist/tools/mcp/registry.js.map +1 -1
  34. package/dist/tui/widgets/model-select.d.ts +13 -1
  35. package/dist/tui/widgets/model-select.d.ts.map +1 -1
  36. package/dist/tui/widgets/model-select.js +29 -3
  37. package/dist/tui/widgets/model-select.js.map +1 -1
  38. package/dist/tui/widgets/model-select.test.d.ts +2 -0
  39. package/dist/tui/widgets/model-select.test.d.ts.map +1 -0
  40. package/dist/tui/widgets/model-select.test.js +38 -0
  41. package/dist/tui/widgets/model-select.test.js.map +1 -0
  42. package/dist/types.d.ts +5 -0
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/dist/config/hello.d.ts +0 -2
  46. package/dist/config/hello.d.ts.map +0 -1
  47. package/dist/config/hello.js +0 -3
  48. package/dist/config/hello.js.map +0 -1
@@ -1,5 +1,64 @@
1
1
  // Health checking for models
2
2
  const healthCache = new Map();
3
+ const catalogCache = new Map();
4
+ const CATALOG_TTL = 5 * 60 * 1000;
5
+ function catalogKey(endpoint) {
6
+ let baseUrl = endpoint;
7
+ if (!/^https?:\/\//i.test(baseUrl)) {
8
+ baseUrl = 'https://' + baseUrl;
9
+ }
10
+ return baseUrl;
11
+ }
12
+ async function fetchEndpointCatalog(endpoint, apiKey) {
13
+ const controller = new AbortController();
14
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
15
+ try {
16
+ const headers = { 'Content-Type': 'application/json' };
17
+ if (apiKey) {
18
+ headers['Authorization'] = `Bearer ${apiKey}`;
19
+ }
20
+ const response = await fetch(`${catalogKey(endpoint)}/models`, {
21
+ signal: controller.signal,
22
+ headers,
23
+ });
24
+ if (!response.ok)
25
+ return null;
26
+ const data = await response.json();
27
+ const ids = new Set();
28
+ for (const m of data?.data ?? []) {
29
+ if (m && typeof m.id === 'string')
30
+ ids.add(m.id);
31
+ }
32
+ catalogCache.set(endpoint, { ids, fetchedAt: Date.now() });
33
+ return ids;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ finally {
39
+ clearTimeout(timeoutId);
40
+ }
41
+ }
42
+ export function getCachedCatalog(endpoint) {
43
+ const cached = catalogCache.get(endpoint);
44
+ if (cached && Date.now() - cached.fetchedAt < CATALOG_TTL)
45
+ return cached.ids;
46
+ return null;
47
+ }
48
+ export async function getEndpointCatalog(endpoint, apiKey) {
49
+ const cached = getCachedCatalog(endpoint);
50
+ if (cached)
51
+ return cached;
52
+ return fetchEndpointCatalog(endpoint, apiKey);
53
+ }
54
+ function notInCatalogHealth(model, cached) {
55
+ return {
56
+ healthy: false,
57
+ lastCheck: Date.now(),
58
+ error: `Model '${model.model}' is not in the catalog served by this endpoint`,
59
+ consecutiveFailures: (cached?.consecutiveFailures ?? 0) + 1,
60
+ };
61
+ }
3
62
  export async function checkModelHealth(model, timeout) {
4
63
  const cacheKey = `${model.endpoint}|${model.model}`;
5
64
  const cached = healthCache.get(cacheKey);
@@ -7,6 +66,13 @@ export async function checkModelHealth(model, timeout) {
7
66
  if (cached && Date.now() - cached.lastCheck < 30000) {
8
67
  return cached;
9
68
  }
69
+ // Short-circuit when the endpoint catalog is cached and the model id is absent
70
+ const cachedCatalog = getCachedCatalog(model.endpoint);
71
+ if (model.model !== 'auto' && cachedCatalog && !cachedCatalog.has(model.model)) {
72
+ const health = notInCatalogHealth(model, cached);
73
+ healthCache.set(cacheKey, health);
74
+ return health;
75
+ }
10
76
  try {
11
77
  const start = Date.now();
12
78
  const controller = new AbortController();
@@ -28,6 +94,25 @@ export async function checkModelHealth(model, timeout) {
28
94
  clearTimeout(timeoutId);
29
95
  const latency = Date.now() - start;
30
96
  if (response.ok) {
97
+ const ids = new Set();
98
+ try {
99
+ const data = await response.json();
100
+ for (const m of data?.data ?? []) {
101
+ if (m && typeof m.id === 'string')
102
+ ids.add(m.id);
103
+ }
104
+ }
105
+ catch {
106
+ // Non-JSON /models response — catalog unknown, skip membership check
107
+ }
108
+ if (ids.size > 0) {
109
+ catalogCache.set(model.endpoint, { ids, fetchedAt: Date.now() });
110
+ }
111
+ if (model.model !== 'auto' && ids.size > 0 && !ids.has(model.model)) {
112
+ const health = notInCatalogHealth(model, cached);
113
+ healthCache.set(cacheKey, health);
114
+ return health;
115
+ }
31
116
  const health = {
32
117
  healthy: true,
33
118
  lastCheck: Date.now(),
@@ -1 +1 @@
1
- {"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/router/health.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/router/health.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, afterEach } from 'vitest';
2
- import { checkModelHealth, getCachedHealth, markHealthy, markUnhealthy, } from './health.js';
2
+ import { checkModelHealth, getCachedHealth, getEndpointCatalog, markHealthy, markUnhealthy, } from './health.js';
3
3
  let modelCounter = 0;
4
4
  function makeModel(overrides = {}) {
5
5
  modelCounter++;
@@ -83,5 +83,42 @@ describe('Model health checking', () => {
83
83
  expect(fetchSpy).toHaveBeenCalled();
84
84
  fetchSpy.mockRestore();
85
85
  });
86
+ it('marks a model unhealthy when its id is missing from the endpoint catalog', async () => {
87
+ const model = makeModel({ model: 'openai/gpt-4.1' });
88
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue({
89
+ ok: true,
90
+ status: 200,
91
+ json: async () => ({ data: [{ id: 'openai/gpt-oss-120b' }, { id: 'openai/gpt-5' }] }),
92
+ });
93
+ const health = await checkModelHealth(model, 1000);
94
+ expect(health.healthy).toBe(false);
95
+ expect(health.error).toContain('not in the catalog');
96
+ });
97
+ it('marks a model healthy when its id is in the endpoint catalog', async () => {
98
+ const model = makeModel({ model: 'openai/gpt-oss-120b' });
99
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue({
100
+ ok: true,
101
+ status: 200,
102
+ json: async () => ({ data: [{ id: 'openai/gpt-oss-120b' }] }),
103
+ });
104
+ const health = await checkModelHealth(model, 1000);
105
+ expect(health.healthy).toBe(true);
106
+ });
107
+ it('treats auto-routed models as catalog-agnostic', async () => {
108
+ const model = makeModel({ model: 'auto' });
109
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue({
110
+ ok: true,
111
+ status: 200,
112
+ json: async () => ({ data: [{ id: 'some-unlisted-id' }] }),
113
+ });
114
+ const health = await checkModelHealth(model, 1000);
115
+ expect(health.healthy).toBe(true);
116
+ });
117
+ it('getEndpointCatalog returns null when the fetch fails', async () => {
118
+ const model = makeModel();
119
+ vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('network down'));
120
+ const catalog = await getEndpointCatalog(model.endpoint, model.apiKey);
121
+ expect(catalog).toBeNull();
122
+ });
86
123
  });
87
124
  //# sourceMappingURL=health.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"health.test.js","sourceRoot":"","sources":["../../src/router/health.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"health.test.js","sourceRoot":"","sources":["../../src/router/health.test.ts"],"names":[],"mappings":"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"}
@@ -12,8 +12,38 @@ export declare class LocalRouter {
12
12
  lastRoutedModel?: string;
13
13
  lastRoutedModelName?: string;
14
14
  lastRoutedTier?: string;
15
+ lastRouteDecision?: {
16
+ model: ModelEntry;
17
+ reason: string;
18
+ skipped: Array<{
19
+ endpoint: string;
20
+ model: string;
21
+ reason: string;
22
+ }>;
23
+ };
15
24
  private routeStats;
25
+ private sessionBlacklist;
26
+ private latencyEma;
27
+ private readonly slowAlpha;
16
28
  constructor(config: RouterConfig);
29
+ getSessionBlacklist(): Array<{
30
+ endpoint: string;
31
+ model: string;
32
+ reason: string;
33
+ at: number;
34
+ }>;
35
+ getLastRouteDecision(): typeof this.lastRouteDecision;
36
+ getLatencyEma(): Array<{
37
+ endpoint: string;
38
+ model: string;
39
+ emaMs: number;
40
+ thresholdMs: number;
41
+ }>;
42
+ clearSessionBlacklist(): void;
43
+ addToSessionBlacklist(endpoint: string, model: string, reason: string): void;
44
+ private blacklistModel;
45
+ private isBlacklisted;
46
+ private recordLatency;
17
47
  private initializeRateLimiters;
18
48
  private getOrCreateClient;
19
49
  startHealthChecks(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/router/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAC;AACpF,OAAO,KAAK,EAAE,WAAW,EAA0C,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC;AAIxD,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,OAAO,CAAkC;IACjD,OAAO,CAAC,gBAAgB,CAA+B;IACvD,OAAO,CAAC,YAAY,CAAgE;IACpF,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,gBAAgB,CAAkC;IACnD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,UAAU,CAAqD;gBAE3D,MAAM,EAAE,YAAY;IAKhC,OAAO,CAAC,sBAAsB;IAW9B,OAAO,CAAC,iBAAiB;IAenB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBlC,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;YAOzB,eAAe;IAoB7B,gBAAgB,IAAI,UAAU,EAAE;IAIhC,gBAAgB,IAAI,UAAU,EAAE;IAYhC,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAgBnD,KAAK,CAAC,OAAO,EAAE,WAAW,EAAE,cAAc,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC;IAsLrF,OAAO,CAAC,cAAc;IAuBhB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IA4D1D,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc,CAAC,WAAW,CAAC;YAqEtD,aAAa;IAmBrB,UAAU,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAcrC,SAAS,IAAI,YAAY;IAIzB,aAAa;;;;;;IAIb,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,IAAI;IAgBjD,IAAI,IAAI;;8BAGgB,WAAW;;MAGlC;CACF;AAWD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,UAAU,GAAG,0BAA0B,EAAE,CAgDjH;AAGD,wBAAgB,YAAY,CAAC,MAAM,GAAE,OAAO,CAAC,YAAY,CAAM,GAAG,WAAW,CAU5E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/router/index.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,23 @@
1
1
  // Daedalus Local Router - Main routing logic
2
2
  import { OpenAI } from 'openai';
3
3
  import { createTokenBucket, consumeTokens, getWaitTime } from './rate-limiter.js';
4
- import { checkModelHealth, getCachedHealth, markHealthy, markUnhealthy } from './health.js';
4
+ import { checkModelHealth, getCachedHealth, getEndpointCatalog, markHealthy, markUnhealthy } from './health.js';
5
+ function isHardFailure(err) {
6
+ const msg = err instanceof Error ? err.message : String(err);
7
+ if (/not in the catalog/i.test(msg))
8
+ return true;
9
+ if (err instanceof Error && (err.name === 'AbortError' || err.name === 'APIUserAbortError'))
10
+ return true;
11
+ if (err instanceof Error && err.name === 'APIConnectionTimeoutError')
12
+ return true;
13
+ if (/timeout|timed out|timedout/i.test(msg))
14
+ return true;
15
+ const status = err?.status
16
+ ?? err?.statusCode;
17
+ if (typeof status === 'number' && status >= 400 && status < 500 && status !== 429)
18
+ return true;
19
+ return false;
20
+ }
5
21
  export class LocalRouter {
6
22
  config;
7
23
  clients = new Map();
@@ -12,11 +28,63 @@ export class LocalRouter {
12
28
  lastRoutedModel;
13
29
  lastRoutedModelName;
14
30
  lastRoutedTier;
31
+ lastRouteDecision;
15
32
  routeStats = { fast: 0, standard: 0, complex: 0, override: 0 };
33
+ sessionBlacklist = new Map();
34
+ latencyEma = new Map();
35
+ slowAlpha = 0.3;
16
36
  constructor(config) {
17
37
  this.config = config;
18
38
  this.initializeRateLimiters();
19
39
  }
40
+ getSessionBlacklist() {
41
+ return Array.from(this.sessionBlacklist.entries()).map(([key, entry]) => {
42
+ const [endpoint, model] = key.split('|');
43
+ return { endpoint, model, reason: entry.reason, at: entry.at };
44
+ });
45
+ }
46
+ getLastRouteDecision() {
47
+ return this.lastRouteDecision;
48
+ }
49
+ getLatencyEma() {
50
+ const threshold = this.config.slowModelThresholdMs ?? 0;
51
+ return Array.from(this.latencyEma.entries()).map(([key, ema]) => {
52
+ const [endpoint, model] = key.split('|');
53
+ return { endpoint, model, emaMs: Math.round(ema), thresholdMs: threshold };
54
+ });
55
+ }
56
+ clearSessionBlacklist() {
57
+ this.sessionBlacklist.clear();
58
+ }
59
+ addToSessionBlacklist(endpoint, model, reason) {
60
+ const entry = this.config.chain.find(m => m.endpoint === endpoint && m.model === model);
61
+ if (entry) {
62
+ this.blacklistModel(entry, reason);
63
+ }
64
+ else {
65
+ this.sessionBlacklist.set(`${endpoint}|${model}`, { reason, at: Date.now() });
66
+ markUnhealthy({ endpoint, model }, reason);
67
+ }
68
+ }
69
+ blacklistModel(m, reason) {
70
+ this.sessionBlacklist.set(`${m.endpoint}|${m.model}`, { reason, at: Date.now() });
71
+ markUnhealthy(m, reason);
72
+ }
73
+ isBlacklisted(m) {
74
+ return this.sessionBlacklist.has(`${m.endpoint}|${m.model}`);
75
+ }
76
+ recordLatency(m, latencyMs) {
77
+ const threshold = this.config.slowModelThresholdMs ?? 0;
78
+ if (threshold <= 0 || this.isBlacklisted(m))
79
+ return;
80
+ const key = `${m.endpoint}|${m.model}`;
81
+ const prev = this.latencyEma.get(key) ?? latencyMs;
82
+ const ema = prev + this.slowAlpha * (latencyMs - prev);
83
+ this.latencyEma.set(key, ema);
84
+ if (ema > threshold) {
85
+ this.blacklistModel(m, `Avg latency ${Math.round(ema)}ms exceeds threshold ${threshold}ms`);
86
+ }
87
+ }
20
88
  initializeRateLimiters() {
21
89
  for (const model of this.config.chain) {
22
90
  if (model.enabled) {
@@ -75,8 +143,19 @@ export class LocalRouter {
75
143
  }
76
144
  await Promise.all(Array.from(uniqueEndpoints.values()).map(async (m) => {
77
145
  const health = await checkModelHealth(m, 5000);
146
+ const catalog = await getEndpointCatalog(m.endpoint, m.apiKey);
147
+ const endpointDown = !health.healthy && !/not in the catalog/i.test(health.error ?? '');
78
148
  for (const target of enabledModels.filter(e => e.endpoint === m.endpoint)) {
79
- if (health.healthy) {
149
+ if (this.isBlacklisted(target))
150
+ continue;
151
+ if (endpointDown) {
152
+ markUnhealthy(target, health.error ?? 'unhealthy');
153
+ continue;
154
+ }
155
+ if (target.model !== 'auto' && catalog && !catalog.has(target.model)) {
156
+ markUnhealthy(target, `Model '${target.model}' is not in the catalog served by this endpoint`);
157
+ }
158
+ else {
80
159
  markHealthy(target, health.latencyMs ?? 0);
81
160
  }
82
161
  }
@@ -108,6 +187,8 @@ export class LocalRouter {
108
187
  continue;
109
188
  if (candidate.supportsTools === false)
110
189
  continue;
190
+ if (this.isBlacklisted(candidate))
191
+ continue;
111
192
  const health = getCachedHealth(candidate);
112
193
  if (health?.healthy === false)
113
194
  continue;
@@ -116,12 +197,51 @@ export class LocalRouter {
116
197
  return undefined;
117
198
  }
118
199
  async route(request, excludedModels) {
119
- let healthyModels = this.getHealthyModels();
200
+ const skipped = [];
201
+ const noteSkipped = (m, reason) => {
202
+ if (!skipped.some(s => s.endpoint === m.endpoint && s.model === m.model && s.reason === reason)) {
203
+ skipped.push({ endpoint: m.endpoint, model: m.model, reason });
204
+ }
205
+ };
206
+ for (const m of this.getEnabledModels()) {
207
+ if (this.isBlacklisted(m))
208
+ noteSkipped(m, 'session-blacklisted');
209
+ }
210
+ let healthyModels = this.getHealthyModels().filter(m => {
211
+ if (this.isBlacklisted(m)) {
212
+ noteSkipped(m, 'session-blacklisted');
213
+ return false;
214
+ }
215
+ return true;
216
+ });
120
217
  if (excludedModels && excludedModels.size > 0) {
121
- healthyModels = healthyModels.filter(m => !excludedModels.has(m.name) && !excludedModels.has(m.model));
218
+ healthyModels = healthyModels.filter(m => {
219
+ if (excludedModels.has(m.name) || excludedModels.has(m.model)) {
220
+ noteSkipped(m, 'excluded-this-turn');
221
+ return false;
222
+ }
223
+ return true;
224
+ });
122
225
  }
123
226
  if (healthyModels.length === 0) {
124
- healthyModels = this.config.chain.filter(m => m.enabled && (!excludedModels || (!excludedModels.has(m.name) && !excludedModels.has(m.model))));
227
+ healthyModels = this.config.chain.filter(m => {
228
+ if (m.enabled && !this.isBlacklisted(m) &&
229
+ (!excludedModels || (!excludedModels.has(m.name) && !excludedModels.has(m.model)))) {
230
+ return true;
231
+ }
232
+ if (m.enabled && this.isBlacklisted(m)) {
233
+ noteSkipped(m, 'session-blacklisted');
234
+ }
235
+ return false;
236
+ });
237
+ }
238
+ if (healthyModels.length === 0) {
239
+ healthyModels = this.config.chain.filter(m => {
240
+ if (m.enabled && (!excludedModels || (!excludedModels.has(m.name) && !excludedModels.has(m.model)))) {
241
+ return true;
242
+ }
243
+ return false;
244
+ });
125
245
  }
126
246
  if (healthyModels.length === 0) {
127
247
  throw new Error('No healthy models available. Check your local servers (LM Studio, Ollama, etc.)');
@@ -159,7 +279,12 @@ export class LocalRouter {
159
279
  const health = getCachedHealth(selectedModel) ?? { healthy: true, lastCheck: Date.now(), consecutiveFailures: 0 };
160
280
  this.routeStats.override++;
161
281
  this.lastRoutedTier = selectedModel.tier;
162
- return { model: selectedModel, health };
282
+ this.lastRouteDecision = {
283
+ model: selectedModel,
284
+ reason: `model override '${request.model}'`,
285
+ skipped: skipped.map(s => ({ ...s })),
286
+ };
287
+ return { model: selectedModel, health, reason: `model override '${request.model}'`, skipped: skipped.map(s => ({ ...s })) };
163
288
  }
164
289
  }
165
290
  }
@@ -278,7 +403,9 @@ export class LocalRouter {
278
403
  }
279
404
  this.lastRoutedTier = targetTier === 'fast' ? 'fast' : targetTier === 'standard' ? 'standard' : 'intelligence';
280
405
  const health = getCachedHealth(selectedModel) ?? { healthy: true, lastCheck: Date.now(), consecutiveFailures: 0 };
281
- return { model: selectedModel, health };
406
+ const reason = `tier '${targetTier}' via ${this.config.strategy} strategy`;
407
+ this.lastRouteDecision = { model: selectedModel, reason, skipped: skipped.map(s => ({ ...s })) };
408
+ return { model: selectedModel, health, reason, skipped: skipped.map(s => ({ ...s })) };
282
409
  }
283
410
  estimateTokens(request) {
284
411
  let totalChars = 0;
@@ -316,6 +443,13 @@ export class LocalRouter {
316
443
  try {
317
444
  const { model } = await this.route(request, excludedModels);
318
445
  selectedModel = model;
446
+ if (process.env.DAEDALUS_DEBUG) {
447
+ const skipped = this.lastRouteDecision?.skipped ?? [];
448
+ const skipInfo = skipped.length > 0
449
+ ? ` (skipped ${skipped.length}: ${skipped.map(s => `${s.model}[${s.reason}]`).join(', ')})`
450
+ : '';
451
+ console.error(`[ROUTE] ${model.name} — ${this.lastRouteDecision?.reason ?? 'n/a'}${skipInfo}`);
452
+ }
319
453
  const client = this.getOrCreateClient(model);
320
454
  const key = `${model.endpoint}|${model.model}`;
321
455
  const actualModel = model.model === 'auto'
@@ -325,7 +459,7 @@ export class LocalRouter {
325
459
  this.lastRoutedModelName = model.name;
326
460
  const { signal, ...body } = request;
327
461
  delete body.complexity;
328
- const isOfficialOpenAI = model.endpoint.includes('api.openai.com');
462
+ const isOfficialOpenAI = model.provider === 'openai' || model.endpoint.includes('api.openai.com');
329
463
  if (body.tool_choice === 'required' && !isOfficialOpenAI) {
330
464
  body.tool_choice = 'auto';
331
465
  }
@@ -342,7 +476,9 @@ export class LocalRouter {
342
476
  messages: sanitizedMessages,
343
477
  model: actualModel,
344
478
  }, { signal });
345
- markHealthy(model, Date.now() - start);
479
+ const elapsed = Date.now() - start;
480
+ markHealthy(model, elapsed);
481
+ this.recordLatency(model, elapsed);
346
482
  return response;
347
483
  }
348
484
  catch (err) {
@@ -350,7 +486,12 @@ export class LocalRouter {
350
486
  if (err instanceof Error && (err.name === 'AbortError' || err.name === 'APIUserAbortError'))
351
487
  throw err;
352
488
  if (selectedModel) {
353
- markUnhealthy(selectedModel, err instanceof Error ? err.message : String(err));
489
+ if (isHardFailure(err)) {
490
+ this.blacklistModel(selectedModel, err instanceof Error ? err.message : String(err));
491
+ }
492
+ else {
493
+ markUnhealthy(selectedModel, err instanceof Error ? err.message : String(err));
494
+ }
354
495
  excludedModels.add(selectedModel.name);
355
496
  excludedModels.add(selectedModel.model);
356
497
  }
@@ -369,6 +510,13 @@ export class LocalRouter {
369
510
  try {
370
511
  const { model } = await this.route(request, excludedModels);
371
512
  selectedModel = model;
513
+ if (process.env.DAEDALUS_DEBUG) {
514
+ const skipped = this.lastRouteDecision?.skipped ?? [];
515
+ const skipInfo = skipped.length > 0
516
+ ? ` (skipped ${skipped.length}: ${skipped.map(s => `${s.model}[${s.reason}]`).join(', ')})`
517
+ : '';
518
+ console.error(`[ROUTE] ${model.name} — ${this.lastRouteDecision?.reason ?? 'n/a'}${skipInfo}`);
519
+ }
372
520
  const client = this.getOrCreateClient(model);
373
521
  const key = `${model.endpoint}|${model.model}`;
374
522
  const actualModel = model.model === 'auto'
@@ -378,7 +526,7 @@ export class LocalRouter {
378
526
  this.lastRoutedModelName = model.name;
379
527
  const { signal, ...body } = request;
380
528
  delete body.complexity;
381
- const isOfficialOpenAI = model.endpoint.includes('api.openai.com');
529
+ const isOfficialOpenAI = model.provider === 'openai' || model.endpoint.includes('api.openai.com');
382
530
  if (body.tool_choice === 'required' && !isOfficialOpenAI) {
383
531
  body.tool_choice = 'auto';
384
532
  }
@@ -401,7 +549,9 @@ export class LocalRouter {
401
549
  for await (const chunk of stream) {
402
550
  yield chunk;
403
551
  }
404
- markHealthy(model, Date.now() - start);
552
+ const elapsed = Date.now() - start;
553
+ markHealthy(model, elapsed);
554
+ this.recordLatency(model, elapsed);
405
555
  return;
406
556
  }
407
557
  catch (err) {
@@ -409,7 +559,12 @@ export class LocalRouter {
409
559
  if (err instanceof Error && (err.name === 'AbortError' || err.name === 'APIUserAbortError'))
410
560
  throw err;
411
561
  if (selectedModel) {
412
- markUnhealthy(selectedModel, err instanceof Error ? err.message : String(err));
562
+ if (isHardFailure(err)) {
563
+ this.blacklistModel(selectedModel, err instanceof Error ? err.message : String(err));
564
+ }
565
+ else {
566
+ markUnhealthy(selectedModel, err instanceof Error ? err.message : String(err));
567
+ }
413
568
  excludedModels.add(selectedModel.name);
414
569
  excludedModels.add(selectedModel.model);
415
570
  }