@almadar/llm 2.22.0 → 2.24.0

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.
@@ -56,6 +56,8 @@ function isChatCompletionMessage(value) {
56
56
  }
57
57
 
58
58
  // src/client.ts
59
+ import { createLogger } from "@almadar/logger";
60
+ var log = createLogger("almadar:llm:client");
59
61
  function cacheTokensFromUsageMetadata(usageMeta) {
60
62
  const details = usageMeta.input_token_details ?? {};
61
63
  return {
@@ -83,6 +85,11 @@ function addCacheControlToSystemMessages(messages) {
83
85
  };
84
86
  });
85
87
  }
88
+ var PROVIDER_BASE_URLS = {
89
+ deepseek: "https://api.deepseek.com/v1",
90
+ kimi: "https://api.moonshot.ai/v1",
91
+ openrouter: "https://openrouter.ai/api/v1"
92
+ };
86
93
  var PROVIDER_CONFIGS = {
87
94
  openai: () => {
88
95
  const apiKey = process.env.OPENAI_API_KEY;
@@ -102,7 +109,7 @@ var PROVIDER_CONFIGS = {
102
109
  }
103
110
  return {
104
111
  apiKey,
105
- baseUrl: "https://api.deepseek.com/v1",
112
+ baseUrl: PROVIDER_BASE_URLS.deepseek,
106
113
  defaultModel: "deepseek-v4-flash"
107
114
  };
108
115
  },
@@ -128,7 +135,7 @@ var PROVIDER_CONFIGS = {
128
135
  }
129
136
  return {
130
137
  apiKey,
131
- baseUrl: "https://api.moonshot.ai/v1",
138
+ baseUrl: PROVIDER_BASE_URLS.kimi,
132
139
  defaultModel: "kimi-k2.5"
133
140
  };
134
141
  },
@@ -141,7 +148,7 @@ var PROVIDER_CONFIGS = {
141
148
  }
142
149
  return {
143
150
  apiKey,
144
- baseUrl: "https://openrouter.ai/api/v1",
151
+ baseUrl: PROVIDER_BASE_URLS.openrouter,
145
152
  defaultModel: "qwen/qwen-2.5-72b-instruct"
146
153
  // Default to Qwen 2.5
147
154
  };
@@ -229,14 +236,18 @@ var _LLMClient = class _LLMClient {
229
236
  this.temperature = options.temperature ?? (this.provider === "kimi" ? 0.6 : DEFAULT_TEMPERATURE);
230
237
  this.streaming = options.streaming ?? false;
231
238
  this.providerConfig = options.providerConfig ?? PROVIDER_CONFIGS[this.provider]();
239
+ const defaultBaseUrl = PROVIDER_BASE_URLS[this.provider];
240
+ if (this.providerConfig.baseUrl === void 0 && defaultBaseUrl) {
241
+ this.providerConfig = { ...this.providerConfig, baseUrl: defaultBaseUrl };
242
+ }
232
243
  this.modelName = options.model || this.providerConfig.defaultModel;
233
244
  this.rawTimeoutMs = options.rawTimeoutMs ?? _LLMClient.DEFAULT_RAW_TIMEOUT_MS;
234
245
  const keyPreview = this.providerConfig.apiKey.slice(-4);
235
- console.log(
246
+ log.info(
236
247
  `[LLMClient] Provider: ${this.provider}, Model: ${this.modelName}, Key: ****${keyPreview}`
237
248
  );
238
249
  if (this.providerConfig.baseUrl) {
239
- console.log(
250
+ log.info(
240
251
  `[LLMClient] Using custom base URL: ${this.providerConfig.baseUrl}`
241
252
  );
242
253
  }
@@ -270,7 +281,7 @@ var _LLMClient = class _LLMClient {
270
281
  const inputTokens = usage.input_tokens ?? 0;
271
282
  const outputTokens = usage.output_tokens ?? 0;
272
283
  if (cacheCreated > 0) {
273
- console.log(
284
+ log.info(
274
285
  `[LLMClient:Anthropic] Cache WRITE: ${cacheCreated} tokens cached`
275
286
  );
276
287
  }
@@ -278,17 +289,17 @@ var _LLMClient = class _LLMClient {
278
289
  const savingsPercent = Math.round(
279
290
  cacheRead / (cacheRead + inputTokens) * 100
280
291
  );
281
- console.log(
292
+ log.info(
282
293
  `[LLMClient:Anthropic] Cache HIT: ${cacheRead} tokens (~${savingsPercent}% of prompt)`
283
294
  );
284
295
  }
285
296
  if (cacheCreated === 0 && cacheRead === 0 && inputTokens > 0) {
286
297
  if (inputTokens < 500) {
287
- console.log(
298
+ log.info(
288
299
  `[LLMClient:Anthropic] ${inputTokens} input, ${outputTokens} output tokens (likely cached)`
289
300
  );
290
301
  } else {
291
- console.log(
302
+ log.info(
292
303
  `[LLMClient:Anthropic] ${inputTokens} input, ${outputTokens} output tokens`
293
304
  );
294
305
  }
@@ -383,21 +394,21 @@ ${prompt}`;
383
394
  } = options;
384
395
  let currentPrompt = userPrompt;
385
396
  let lastError = null;
386
- console.log(
397
+ log.info(
387
398
  `[LLMClient:call] Starting call to ${this.provider}/${this.modelName}`
388
399
  );
389
- console.log(`[LLMClient:call] Prompt length: ${userPrompt.length} chars`);
400
+ log.info(`[LLMClient:call] Prompt length: ${userPrompt.length} chars`);
390
401
  if (maxTokens) {
391
- console.log(`[LLMClient:call] Max tokens: ${maxTokens}`);
402
+ log.info(`[LLMClient:call] Max tokens: ${maxTokens}`);
392
403
  }
393
404
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
394
405
  try {
395
- console.log(
406
+ log.info(
396
407
  `[LLMClient:call] Attempt ${attempt + 1}/${maxRetries + 1}...`
397
408
  );
398
409
  const attemptStartTime = Date.now();
399
410
  const result = await this.rateLimiter.execute(async () => {
400
- console.log(`[LLMClient:call] Invoking model...`);
411
+ log.info(`[LLMClient:call] Invoking model...`);
401
412
  const invokeStartTime = Date.now();
402
413
  const modelToUse = maxTokens || temperature !== void 0 ? this.getModelWithOptions({ maxTokens, temperature }) : this.model;
403
414
  const messages = [
@@ -407,7 +418,7 @@ ${prompt}`;
407
418
  const response = await modelToUse.invoke(
408
419
  this.provider === "anthropic" ? addCacheControlToSystemMessages(messages) : messages
409
420
  );
410
- console.log(
421
+ log.info(
411
422
  `[LLMClient:call] Model responded in ${Date.now() - invokeStartTime}ms`
412
423
  );
413
424
  let usage = null;
@@ -418,7 +429,7 @@ ${prompt}`;
418
429
  completionTokens: usageMeta.output_tokens || 0,
419
430
  totalTokens: (usageMeta.input_tokens || 0) + (usageMeta.output_tokens || 0)
420
431
  };
421
- console.log(
432
+ log.info(
422
433
  `[LLMClient:call] Tokens used: ${usage.promptTokens} in, ${usage.completionTokens} out`
423
434
  );
424
435
  if (this.tokenTracker) {
@@ -431,21 +442,21 @@ ${prompt}`;
431
442
  }
432
443
  const finishReason = this.extractFinishReason(response);
433
444
  if (finishReason === "length") {
434
- console.warn(
445
+ log.warn(
435
446
  `[LLMClient:call] Response truncated (finish_reason=length)`
436
447
  );
437
448
  }
438
449
  const content = typeof response.content === "string" ? response.content : JSON.stringify(response.content);
439
- console.log(
450
+ log.info(
440
451
  `[LLMClient:call] Response length: ${content.length} chars, finish_reason: ${finishReason}`
441
452
  );
442
453
  return { content, finishReason, usage };
443
454
  });
444
- console.log(
455
+ log.info(
445
456
  `[LLMClient:call] Attempt ${attempt + 1} completed in ${Date.now() - attemptStartTime}ms, parsing response...`
446
457
  );
447
458
  const parsed = skipSchemaValidation ? parseJsonResponse(result.content, void 0) : parseJsonResponse(result.content, schema);
448
- console.log(
459
+ log.info(
449
460
  `[LLMClient:call] Response parsed successfully${skipSchemaValidation ? " (schema validation skipped)" : ""}`
450
461
  );
451
462
  return {
@@ -456,16 +467,15 @@ ${prompt}`;
456
467
  };
457
468
  } catch (error) {
458
469
  lastError = error instanceof Error ? error : new Error(String(error));
459
- console.error(
460
- `[LLMClient:call] Attempt ${attempt + 1} failed:`,
461
- lastError.message
470
+ log.error(
471
+ `[LLMClient:call] Attempt ${attempt + 1} failed: ${lastError.message}`
462
472
  );
463
473
  if (this.isRateLimitError(lastError)) {
464
- console.error(`[LLMClient:call] Rate limit error, not retrying`);
474
+ log.error(`[LLMClient:call] Rate limit error, not retrying`);
465
475
  throw lastError;
466
476
  }
467
477
  if (attempt < maxRetries && retryWithContext) {
468
- console.log(`[LLMClient:call] Will retry with error context`);
478
+ log.info(`[LLMClient:call] Will retry with error context`);
469
479
  currentPrompt = `${userPrompt}
470
480
 
471
481
  [Previous attempt failed with: ${lastError.message}]
@@ -473,7 +483,7 @@ Please output valid JSON that matches the expected schema.`;
473
483
  }
474
484
  }
475
485
  }
476
- console.error(`[LLMClient:call] All attempts exhausted, throwing error`);
486
+ log.error(`[LLMClient:call] All attempts exhausted, throwing error`);
477
487
  throw lastError;
478
488
  }
479
489
  extractFinishReason(response) {
@@ -515,21 +525,21 @@ Please output valid JSON that matches the expected schema.`;
515
525
  );
516
526
  }
517
527
  }
518
- console.log(
528
+ log.info(
519
529
  `[LLMClient:${label}] Invoking ${this.provider}/${this.modelName} (timeout=${timeoutMs}ms)`
520
530
  );
521
531
  try {
522
532
  const response = await modelToUse.invoke(langchainMessages, {
523
533
  signal: controller.signal
524
534
  });
525
- console.log(
535
+ log.info(
526
536
  `[LLMClient:${label}] Responded in ${Date.now() - startedAt}ms`
527
537
  );
528
538
  return response;
529
539
  } catch (err) {
530
540
  const elapsed = Date.now() - startedAt;
531
541
  const message = err instanceof Error ? err.message : String(err);
532
- console.error(
542
+ log.error(
533
543
  `[LLMClient:${label}] FAILED after ${elapsed}ms: ${message}`
534
544
  );
535
545
  throw err;
@@ -667,7 +677,7 @@ Please output valid JSON that matches the expected schema.`;
667
677
  body["usage"] = { include: true };
668
678
  }
669
679
  const startedAt = Date.now();
670
- console.log(
680
+ log.info(
671
681
  `[LLMClient:callWithTools] Invoking ${this.provider}/${this.modelName} (tools=${options.tools.length}, messages=${options.messages.length})`
672
682
  );
673
683
  const effectiveTimeoutMs = options.timeoutMs ?? this.rawTimeoutMs;
@@ -695,7 +705,7 @@ Please output valid JSON that matches the expected schema.`;
695
705
  if (!response.ok) {
696
706
  const errText = await response.text().catch(() => "<no body>");
697
707
  const elapsed = Date.now() - startedAt;
698
- console.error(
708
+ log.error(
699
709
  `[LLMClient:callWithTools] FAILED after ${elapsed}ms (${response.status} ${response.statusText}): ${errText}`
700
710
  );
701
711
  throw new Error(
@@ -730,7 +740,7 @@ Please output valid JSON that matches the expected schema.`;
730
740
  });
731
741
  }
732
742
  }
733
- console.log(
743
+ log.info(
734
744
  `[LLMClient:callWithTools] Responded in ${Date.now() - startedAt}ms (prompt=${usage?.promptTokens ?? 0}, completion=${usage?.completionTokens ?? 0}, tool_calls=${choice.message.tool_calls?.length ?? 0})`
735
745
  );
736
746
  return {
@@ -784,19 +794,19 @@ Please output valid JSON that matches the expected schema.`;
784
794
  rawText = false
785
795
  } = options;
786
796
  if (this.provider !== "anthropic") {
787
- console.log(
797
+ log.info(
788
798
  `[LLMClient:callWithCache] Provider ${this.provider} doesn't support caching, using regular call`
789
799
  );
790
800
  return this.callWithMetadata(options);
791
801
  }
792
802
  const cacheableCount = (systemBlocks || []).filter((b) => b.cache_control).length + (userBlocks || []).filter((b) => b.cache_control).length;
793
- console.log(
803
+ log.info(
794
804
  `[LLMClient:callWithCache] ${cacheableCount} cacheable block(s)`
795
805
  );
796
806
  let lastError = null;
797
807
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
798
808
  try {
799
- console.log(
809
+ log.info(
800
810
  `[LLMClient:callWithCache] Attempt ${attempt + 1}/${maxRetries + 1}...`
801
811
  );
802
812
  const result = await this.rateLimiter.execute(async () => {
@@ -824,7 +834,7 @@ Please output valid JSON that matches the expected schema.`;
824
834
  const cacheRead = apiUsage.cache_read_input_tokens || 0;
825
835
  const cacheCreation = apiUsage.cache_creation_input_tokens || 0;
826
836
  if (cacheCreation > 0) {
827
- console.log(
837
+ log.info(
828
838
  `[LLMClient:callWithCache] Cache WRITE: ${cacheCreation} tokens`
829
839
  );
830
840
  }
@@ -832,12 +842,12 @@ Please output valid JSON that matches the expected schema.`;
832
842
  const savingsPercent = Math.round(
833
843
  cacheRead / (cacheRead + apiUsage.input_tokens) * 100
834
844
  );
835
- console.log(
845
+ log.info(
836
846
  `[LLMClient:callWithCache] Cache HIT: ${cacheRead} tokens (~${savingsPercent}% of prompt)`
837
847
  );
838
848
  }
839
849
  if (cacheCreation === 0 && cacheRead === 0) {
840
- console.log(
850
+ log.info(
841
851
  `[LLMClient:callWithCache] No caching: ${apiUsage.input_tokens} input tokens`
842
852
  );
843
853
  }
@@ -876,9 +886,8 @@ Please output valid JSON that matches the expected schema.`;
876
886
  };
877
887
  } catch (error) {
878
888
  lastError = error instanceof Error ? error : new Error(String(error));
879
- console.error(
880
- `[LLMClient:callWithCache] Attempt ${attempt + 1} failed:`,
881
- lastError.message
889
+ log.error(
890
+ `[LLMClient:callWithCache] Attempt ${attempt + 1} failed: ${lastError.message}`
882
891
  );
883
892
  if (this.isRateLimitError(lastError)) {
884
893
  throw lastError;
@@ -1041,4 +1050,4 @@ export {
1041
1050
  createOpenRouterClient,
1042
1051
  createZhipuClient
1043
1052
  };
1044
- //# sourceMappingURL=chunk-G2OE5TBE.js.map
1053
+ //# sourceMappingURL=chunk-V4O6E2VI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/tool-call-types.ts"],"sourcesContent":["/**\n * Shared LLM Client\n *\n * Multi-provider LLM client with:\n * - OpenAI, DeepSeek, Anthropic, and Kimi support\n * - Anthropic prompt caching (CachingChatAnthropic)\n * - Rate limiting and retry logic\n * - Token tracking\n * - Structured output parsing with Zod\n *\n * @packageDocumentation\n */\n\nimport { ChatOpenAI } from '@langchain/openai';\nimport { ChatAnthropic } from '@langchain/anthropic';\nimport type { BaseMessageLike } from '@langchain/core/messages';\nimport Anthropic from '@anthropic-ai/sdk';\nimport { z } from 'zod';\nimport {\n RateLimiter,\n getGlobalRateLimiter,\n type RateLimiterOptions,\n} from './rate-limiter.js';\nimport { TokenTracker, getGlobalTokenTracker } from './token-tracker.js';\nimport { parseJsonResponse } from './json-parser.js';\nimport {\n parseChatCompletionResponse,\n type ChatCompletionMessage,\n type ChatCompletionToolDef,\n} from './tool-call-types.js';\nimport { createLogger } from '@almadar/logger';\n\n// Structured logger so callers (e.g. the orb CLI's ink TUI) can silence this\n// chatter via `setRuntimeNamespaceFilter('')`; raw console.log can't be muted.\nconst log = createLogger('almadar:llm:client');\n\n// ============================================================================\n// Local type helpers (avoid Record<string, unknown> and unsafe casts)\n// ============================================================================\n\n/** Anthropic generation output with usage metadata (not in Langchain's base types). */\ninterface AnthropicGenerationWithUsage {\n message?: {\n usage_metadata?: {\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n input_tokens?: number;\n output_tokens?: number;\n };\n };\n}\n\n/** Response metadata from OpenAI-compatible providers. */\ninterface OpenAIResponseMetadata {\n finish_reason?: string;\n}\n\n/** Model-specific kwargs passed to ChatOpenAI constructor. */\ninterface ModelKwargs {\n max_completion_tokens?: number;\n thinking?: { type: string };\n tool_choice?: string;\n /** OpenRouter: ask for detailed usage accounting (returns cached-token breakdown). */\n usage?: { include: boolean };\n}\n\n/**\n * Pull the cache-read / cache-write token split out of a LangChain\n * `usage_metadata` object. Every provider LangChain supports normalises\n * its cache-hit tokens into `input_token_details` (OpenAI/OpenRouter/DeepSeek\n * via `cached_tokens`→`cache_read`; Anthropic via `cache_read`/`cache_creation`),\n * so one extractor covers them all. Returns zeros when the provider reports\n * no cache detail — which prices identically to the old flat-rate path.\n */\nfunction cacheTokensFromUsageMetadata(usageMeta: {\n input_tokens?: number;\n output_tokens?: number;\n input_token_details?: { cache_read?: number; cache_creation?: number };\n}): { cachedPromptTokens: number; cacheWriteTokens: number } {\n const details = usageMeta.input_token_details ?? {};\n return {\n cachedPromptTokens: details.cache_read ?? 0,\n cacheWriteTokens: details.cache_creation ?? 0,\n };\n}\n\n/**\n * Identity cast for generic return types.\n * Used when a string value must satisfy a generic T parameter\n * (e.g., rawText mode where caller declares T = string).\n *\n * Safety: callers only reach this path when rawText=true, which\n * constrains T to string by convention. TypeScript cannot verify\n * this constraint statically because T is caller-supplied.\n */\nfunction asGeneric<T>(value: string): T {\n return value as T;\n}\n\n// ============================================================================\n// Anthropic Cache Control Helper\n// ============================================================================\n\nfunction addCacheControlToSystemMessages(\n messages: Array<{ role: string; content: string }>,\n): BaseMessageLike[] {\n return messages.map((msg) => {\n if (msg.role !== 'system') {\n return msg as BaseMessageLike;\n }\n\n return {\n role: msg.role,\n content: [\n {\n type: 'text' as const,\n text: msg.content,\n cache_control: { type: 'ephemeral' },\n },\n ],\n } as BaseMessageLike;\n });\n}\n\ntype ChatModel = ChatOpenAI | ChatAnthropic;\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type LLMProvider = 'openai' | 'deepseek' | 'anthropic' | 'kimi' | 'openrouter' | 'orbgen' | 'masar';\n\nexport interface ProviderConfig {\n apiKey: string;\n baseUrl?: string;\n defaultModel: string;\n}\n\nexport interface LLMClientOptions {\n provider?: LLMProvider;\n model?: string;\n temperature?: number;\n streaming?: boolean;\n rateLimiter?: RateLimiterOptions;\n useGlobalRateLimiter?: boolean;\n trackTokens?: boolean;\n /**\n * Hard per-call deadline (ms) for the raw invoke paths (`callRaw`,\n * `callRawWithMetadata`, `callWithMessages`). Defaults to 5 min. Set this\n * BELOW the caller's own wall-clock cap so a hung connection aborts in\n * time for the caller to recover (e.g. a battery spec capped at 300s sets\n * 120s here so a hang fails fast and leaves budget for a retry, instead of\n * the call consuming the entire 300s).\n */\n rawTimeoutMs?: number;\n /**\n * Explicit provider credentials. When set, used instead of reading the\n * provider's env vars — lets callers (e.g. the CLI's account store) supply\n * `apiKey`/`baseUrl`/`defaultModel` directly. Absent ⇒ env fallback.\n */\n providerConfig?: ProviderConfig;\n}\n\nexport interface LLMCallOptions<T = unknown> {\n systemPrompt: string;\n userPrompt: string;\n schema?: z.ZodSchema<T>;\n maxRetries?: number;\n retryWithContext?: boolean;\n maxTokens?: number;\n skipSchemaValidation?: boolean;\n temperature?: number;\n}\n\nexport interface CacheableBlock {\n type: 'text';\n text: string;\n cache_control?: { type: 'ephemeral' };\n}\n\nexport interface CacheAwareLLMCallOptions<T = unknown>\n extends LLMCallOptions<T> {\n systemBlocks?: CacheableBlock[];\n userBlocks?: CacheableBlock[];\n rawText?: boolean;\n}\n\nexport interface LLMUsage {\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n}\n\nexport type LLMFinishReason =\n | 'stop'\n | 'length'\n | 'content_filter'\n | 'tool_calls'\n | null;\n\nexport interface LLMResponse<T> {\n data: T;\n raw: string;\n finishReason: LLMFinishReason;\n usage: LLMUsage | null;\n}\n\nexport interface LLMStreamOptions {\n systemPrompt: string;\n messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>;\n maxTokens?: number;\n temperature?: number;\n}\n\nexport interface LLMStreamChunk {\n content: string;\n done: boolean;\n}\n\n// ============================================================================\n// Provider Configuration\n// ============================================================================\n\n/**\n * Canonical base URL per OpenAI-compatible provider — the single source of\n * truth. Used both by the env-mode factories below and by the constructor's\n * back-fill, so an explicit `providerConfig` that omits `baseUrl` (e.g. the\n * CLI's `~/.almadar` account, which only stores apiKey) is sent to the\n * PROVIDER's endpoint, not api.openai.com. openai/anthropic intentionally have\n * none (their SDK defaults are correct); orbgen/masar derive theirs from env.\n */\nconst PROVIDER_BASE_URLS: Partial<Record<LLMProvider, string>> = {\n deepseek: 'https://api.deepseek.com/v1',\n kimi: 'https://api.moonshot.ai/v1',\n openrouter: 'https://openrouter.ai/api/v1',\n};\n\nconst PROVIDER_CONFIGS: Record<LLMProvider, () => ProviderConfig> = {\n openai: () => {\n const apiKey = process.env.OPENAI_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'OPENAI_API_KEY environment variable is not set. ' +\n 'Please set it in your .env file or environment.',\n );\n }\n return { apiKey, baseUrl: undefined, defaultModel: 'gpt-4o' };\n },\n deepseek: () => {\n const apiKey = process.env.DEEPSEEK_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'DEEPSEEK_API_KEY environment variable is not set. ' +\n 'Please set it in your .env file or environment.',\n );\n }\n return {\n apiKey,\n baseUrl: PROVIDER_BASE_URLS.deepseek,\n defaultModel: 'deepseek-v4-flash',\n };\n },\n anthropic: () => {\n const apiKey = process.env.ANTHROPIC_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'ANTHROPIC_API_KEY environment variable is not set. ' +\n 'Please set it in your .env file or environment.',\n );\n }\n return {\n apiKey,\n baseUrl: undefined,\n defaultModel: 'claude-sonnet-4-5-20250929',\n };\n },\n kimi: () => {\n const apiKey = process.env.KIMI_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'KIMI_API_KEY environment variable is not set. ' +\n 'Please set it in your .env file or environment.',\n );\n }\n return {\n apiKey,\n baseUrl: PROVIDER_BASE_URLS.kimi,\n defaultModel: 'kimi-k2.5',\n };\n },\n openrouter: () => {\n const apiKey = process.env.OPEN_ROUTER_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'OPEN_ROUTER_API_KEY environment variable is not set. ' +\n 'Please set it in your .env file or environment.',\n );\n }\n return {\n apiKey,\n baseUrl: PROVIDER_BASE_URLS.openrouter,\n defaultModel: 'qwen/qwen-2.5-72b-instruct', // Default to Qwen 2.5\n };\n },\n orbgen: () => {\n const baseUrl = process.env.ORBGEN_URL;\n if (!baseUrl) {\n throw new Error(\n 'ORBGEN_URL environment variable is not set. ' +\n 'Set it to the OrbGen Cloud Run URL (e.g., https://orbgen-v2-xxx.run.app)',\n );\n }\n return {\n apiKey: 'not-needed',\n baseUrl: `${baseUrl}/v1`,\n defaultModel: 'orbgen-v2',\n };\n },\n masar: () => {\n // Fine-tuned masar subagent served OpenAI-compatibly (vLLM on Cloud Run GPU).\n const baseUrl = process.env.MASAR_SUBAGENT_URL;\n if (!baseUrl) {\n throw new Error(\n 'MASAR_SUBAGENT_URL environment variable is not set. ' +\n 'Set it to the masar subagent endpoint (e.g., https://masar-subagent-xxx.run.app)',\n );\n }\n return {\n apiKey: process.env.MASAR_SUBAGENT_API_KEY ?? 'not-needed',\n baseUrl: `${baseUrl}/v1`,\n defaultModel: process.env.MASAR_SUBAGENT_MODEL ?? 'subagent',\n };\n },\n};\n\nexport const DEEPSEEK_MODELS = {\n CHAT: 'deepseek-chat',\n CODER: 'deepseek-coder',\n REASONER: 'deepseek-reasoner',\n V4_PRO: 'deepseek-v4-pro',\n V4_FLASH: 'deepseek-v4-flash',\n} as const;\n\nexport const OPENAI_MODELS = {\n GPT4O: 'gpt-4o',\n GPT4O_MINI: 'gpt-4o-mini',\n GPT4_TURBO: 'gpt-4-turbo',\n GPT35_TURBO: 'gpt-3.5-turbo',\n GPT_5_1: 'gpt-5.1',\n} as const;\n\nexport const ANTHROPIC_MODELS = {\n CLAUDE_SONNET_4_5: 'claude-sonnet-4-5-20250929',\n CLAUDE_SONNET_4: 'claude-sonnet-4-20250514',\n CLAUDE_OPUS_4_5: 'claude-opus-4-5-20250929',\n CLAUDE_3_5_HAIKU: 'claude-3-5-haiku-20241022',\n} as const;\n\nexport const KIMI_MODELS = {\n K2_5: 'kimi-k2.5',\n} as const;\n\nexport const OPENROUTER_MODELS = {\n // Qwen models - JSON/structured data specialists\n QWEN_2_5_72B: 'qwen/qwen-2.5-72b-instruct',\n QWEN_2_5_CODER_32B: 'qwen/qwen-2.5-coder-32b-instruct',\n QWEN_3_235B: 'qwen/qwen3-235b-a22b',\n\n // Gemma models - best small models for structured JSON output\n // Gemma 3 4B: 6/6 on complex decomposition, 100% behavior matching, free, fastest\n GEMMA_3_4B: 'google/gemma-3-4b-it',\n GEMMA_3_12B: 'google/gemma-3-12b-it',\n GEMMA_3_27B: 'google/gemma-3-27b-it',\n\n // Mistral models - strong structured output, function calling\n MINISTRAL_8B: 'mistralai/ministral-8b-2512',\n // Mistral Small 3.1: 6/6 on complex decomposition, picked std-kanban for tasks\n MISTRAL_SMALL_3_1: 'mistralai/mistral-small-3.1-24b-instruct',\n // Mistral Medium 3.1: next tier up from Small, stronger reasoning, tool calling\n MISTRAL_MEDIUM_3_1: 'mistralai/mistral-medium-3.1',\n\n // Llama models - agentic workhorses\n LLAMA_3_3_70B: 'meta-llama/llama-3.3-70b-instruct',\n LLAMA_3_1_405B: 'meta-llama/llama-3.1-405b-instruct',\n LLAMA_4_MAVERICK: 'meta-llama/llama-4-maverick',\n LLAMA_4_SCOUT: 'meta-llama/llama-4-scout',\n\n // Kimi models - strong reasoning\n KIMI_K2: 'moonshotai/kimi-k2',\n\n // Zhipu GLM models - via OpenRouter\n GLM_4_7: 'z-ai/glm-4.7',\n} as const;\n\nconst DEFAULT_TEMPERATURE = 0.3;\n\n// ============================================================================\n// LLM Client\n// ============================================================================\n\nexport class LLMClient {\n private model: ChatModel;\n private rateLimiter: RateLimiter;\n private tokenTracker: TokenTracker | null;\n private modelName: string;\n private provider: LLMProvider;\n private providerConfig: ProviderConfig;\n private temperature: number;\n private streaming: boolean;\n private rawTimeoutMs: number;\n\n constructor(options: LLMClientOptions = {}) {\n this.provider = options.provider || 'openai';\n // Kimi: 0.6 when thinking disabled (our default), 1.0 when thinking enabled\n this.temperature = options.temperature ?? \n (this.provider === 'kimi' ? 0.6 : DEFAULT_TEMPERATURE);\n this.streaming = options.streaming ?? false;\n\n this.providerConfig = options.providerConfig ?? PROVIDER_CONFIGS[this.provider]();\n // An explicit providerConfig (e.g. from the CLI's ~/.almadar account, which\n // stores only apiKey) may omit baseUrl. Back-fill the PROVIDER's default so\n // the request isn't sent to api.openai.com — the fallback that createModel()\n // and callWithTools() use when baseUrl is undefined. Env-mode already sets\n // the right baseUrl via PROVIDER_CONFIGS, and openai/anthropic correctly\n // stay undefined (no entry in PROVIDER_BASE_URLS).\n const defaultBaseUrl = PROVIDER_BASE_URLS[this.provider];\n if (this.providerConfig.baseUrl === undefined && defaultBaseUrl) {\n this.providerConfig = { ...this.providerConfig, baseUrl: defaultBaseUrl };\n }\n this.modelName = options.model || this.providerConfig.defaultModel;\n this.rawTimeoutMs = options.rawTimeoutMs ?? LLMClient.DEFAULT_RAW_TIMEOUT_MS;\n\n const keyPreview = this.providerConfig.apiKey.slice(-4);\n log.info(\n `[LLMClient] Provider: ${this.provider}, Model: ${this.modelName}, Key: ****${keyPreview}`,\n );\n if (this.providerConfig.baseUrl) {\n log.info(\n `[LLMClient] Using custom base URL: ${this.providerConfig.baseUrl}`,\n );\n }\n\n this.model = this.createModel();\n\n this.rateLimiter =\n options.useGlobalRateLimiter !== false\n ? getGlobalRateLimiter(options.rateLimiter)\n : new RateLimiter(options.rateLimiter);\n\n this.tokenTracker =\n options.trackTokens !== false\n ? getGlobalTokenTracker(this.modelName)\n : null;\n }\n\n private usesMaxCompletionTokens(): boolean {\n const model = this.modelName.toLowerCase();\n return (\n model.startsWith('o1') ||\n model.startsWith('gpt-5') ||\n model.includes('o1-') ||\n model.includes('o3')\n );\n }\n\n private createModel(options?: {\n maxTokens?: number;\n temperature?: number;\n }): ChatModel {\n const maxTokens = options?.maxTokens;\n const temperature = options?.temperature ?? this.temperature;\n\n if (this.provider === 'anthropic') {\n return new ChatAnthropic({\n apiKey: this.providerConfig.apiKey,\n model: this.modelName,\n temperature,\n streaming: this.streaming,\n maxTokens: maxTokens || 8192,\n callbacks: [\n {\n handleLLMEnd: (output) => {\n const generation = output.generations?.[0]?.[0];\n const generationWithUsage = generation as\n | (typeof generation & AnthropicGenerationWithUsage)\n | undefined;\n const usage = generationWithUsage?.message?.usage_metadata;\n\n if (usage) {\n const cacheCreated = usage.cache_creation_input_tokens ?? 0;\n const cacheRead = usage.cache_read_input_tokens ?? 0;\n const inputTokens = usage.input_tokens ?? 0;\n const outputTokens = usage.output_tokens ?? 0;\n\n if (cacheCreated > 0) {\n log.info(\n `[LLMClient:Anthropic] Cache WRITE: ${cacheCreated} tokens cached`,\n );\n }\n if (cacheRead > 0) {\n const savingsPercent = Math.round(\n (cacheRead / (cacheRead + inputTokens)) * 100,\n );\n log.info(\n `[LLMClient:Anthropic] Cache HIT: ${cacheRead} tokens (~${savingsPercent}% of prompt)`,\n );\n }\n if (cacheCreated === 0 && cacheRead === 0 && inputTokens > 0) {\n if (inputTokens < 500) {\n log.info(\n `[LLMClient:Anthropic] ${inputTokens} input, ${outputTokens} output tokens (likely cached)`,\n );\n } else {\n log.info(\n `[LLMClient:Anthropic] ${inputTokens} input, ${outputTokens} output tokens`,\n );\n }\n }\n }\n },\n },\n ],\n });\n }\n\n const useCompletionTokens = this.usesMaxCompletionTokens();\n\n const tokenConfig = maxTokens\n ? useCompletionTokens\n ? { modelKwargs: { max_completion_tokens: maxTokens } }\n : { maxTokens }\n : {};\n\n const timeout = this.provider === 'deepseek' ? 600000 : undefined;\n\n // Kimi-k2.5: disable thinking to avoid reasoning_content issues with tool calls\n // When thinking is disabled, temperature must be 0.6 (not 1.0)\n const isKimi = this.provider === 'kimi';\n const effectiveTemp = isKimi ? 0.6 : temperature;\n\n // Build modelKwargs incrementally to avoid spread conflicts\n const modelKwargs: ModelKwargs = {};\n if (useCompletionTokens && maxTokens) {\n modelKwargs.max_completion_tokens = maxTokens;\n }\n if (isKimi) {\n modelKwargs.thinking = { type: 'disabled' };\n }\n // OpenRouter (Qwen): explicit tool_choice so the model doesn't ignore tool definitions\n if (this.provider === 'openrouter') {\n modelKwargs.tool_choice = 'auto';\n // Return the cached-token breakdown so cost is priced cache-aware.\n modelKwargs.usage = { include: true };\n }\n\n return new ChatOpenAI({\n apiKey: this.providerConfig.apiKey,\n model: this.modelName,\n temperature: useCompletionTokens ? undefined : effectiveTemp,\n streaming: this.streaming,\n timeout,\n ...(Object.keys(modelKwargs).length > 0 ? { modelKwargs } : {}),\n ...(useCompletionTokens ? {} : maxTokens ? { maxTokens } : {}),\n configuration: {\n apiKey: this.providerConfig.apiKey,\n ...(this.providerConfig.baseUrl\n ? { baseURL: this.providerConfig.baseUrl }\n : {}),\n },\n });\n }\n\n private getModelWithOptions(options: {\n maxTokens?: number;\n temperature?: number;\n }): ChatModel {\n return this.createModel(options);\n }\n\n /**\n * Check if this model is a Qwen3.5 thinking model.\n * These models burn all output tokens on internal reasoning\n * unless thinking is explicitly disabled via /no_think prefix.\n */\n private isQwenThinkingModel(): boolean {\n return this.modelName.includes('qwen3.5');\n }\n\n /**\n * Prepare user prompt with provider-specific adjustments.\n * Qwen3.5 models require /no_think to disable reasoning mode.\n */\n private prepareUserPrompt(prompt: string): string {\n if (this.isQwenThinkingModel()) {\n return `/no_think\\n${prompt}`;\n }\n return prompt;\n }\n\n getProvider(): LLMProvider {\n return this.provider;\n }\n\n getModelName(): string {\n return this.modelName;\n }\n\n getModel(): ChatModel {\n return this.model;\n }\n\n getRateLimiterStatus() {\n return this.rateLimiter.getStatus();\n }\n\n getTokenUsage() {\n return this.tokenTracker?.getSummary() ?? null;\n }\n\n async call<T>(options: LLMCallOptions<T>): Promise<T> {\n const response = await this.callWithMetadata(options);\n return response.data;\n }\n\n async callWithMetadata<T>(options: LLMCallOptions<T>): Promise<LLMResponse<T>> {\n const {\n systemPrompt,\n userPrompt,\n schema,\n maxRetries = 2,\n retryWithContext = true,\n maxTokens,\n skipSchemaValidation = false,\n temperature,\n } = options;\n\n let currentPrompt = userPrompt;\n let lastError: Error | null = null;\n\n log.info(\n `[LLMClient:call] Starting call to ${this.provider}/${this.modelName}`,\n );\n log.info(`[LLMClient:call] Prompt length: ${userPrompt.length} chars`);\n if (maxTokens) {\n log.info(`[LLMClient:call] Max tokens: ${maxTokens}`);\n }\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n log.info(\n `[LLMClient:call] Attempt ${attempt + 1}/${maxRetries + 1}...`,\n );\n const attemptStartTime = Date.now();\n\n const result = await this.rateLimiter.execute(async () => {\n log.info(`[LLMClient:call] Invoking model...`);\n const invokeStartTime = Date.now();\n\n const modelToUse =\n maxTokens || temperature !== undefined\n ? this.getModelWithOptions({ maxTokens, temperature })\n : this.model;\n\n const messages = [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: this.prepareUserPrompt(currentPrompt) },\n ];\n const response = await modelToUse.invoke(\n this.provider === 'anthropic'\n ? addCacheControlToSystemMessages(messages)\n : messages,\n );\n\n log.info(\n `[LLMClient:call] Model responded in ${Date.now() - invokeStartTime}ms`,\n );\n\n let usage: LLMUsage | null = null;\n if (response.usage_metadata) {\n const usageMeta = response.usage_metadata as {\n input_tokens?: number;\n output_tokens?: number;\n };\n usage = {\n promptTokens: usageMeta.input_tokens || 0,\n completionTokens: usageMeta.output_tokens || 0,\n totalTokens:\n (usageMeta.input_tokens || 0) +\n (usageMeta.output_tokens || 0),\n };\n log.info(\n `[LLMClient:call] Tokens used: ${usage.promptTokens} in, ${usage.completionTokens} out`,\n );\n\n if (this.tokenTracker) {\n this.tokenTracker.addUsage(\n usage.promptTokens,\n usage.completionTokens,\n { provider: this.provider, ...cacheTokensFromUsageMetadata(usageMeta) },\n );\n }\n }\n\n const finishReason = this.extractFinishReason(response);\n if (finishReason === 'length') {\n log.warn(\n `[LLMClient:call] Response truncated (finish_reason=length)`,\n );\n }\n\n const content =\n typeof response.content === 'string'\n ? response.content\n : JSON.stringify(response.content);\n\n log.info(\n `[LLMClient:call] Response length: ${content.length} chars, finish_reason: ${finishReason}`,\n );\n\n return { content, finishReason, usage };\n });\n\n log.info(\n `[LLMClient:call] Attempt ${attempt + 1} completed in ${Date.now() - attemptStartTime}ms, parsing response...`,\n );\n\n const parsed = skipSchemaValidation\n ? (parseJsonResponse(result.content, undefined) as T)\n : parseJsonResponse(result.content, schema);\n log.info(\n `[LLMClient:call] Response parsed successfully${skipSchemaValidation ? ' (schema validation skipped)' : ''}`,\n );\n\n return {\n data: parsed,\n raw: result.content,\n finishReason: result.finishReason,\n usage: result.usage,\n };\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n log.error(\n `[LLMClient:call] Attempt ${attempt + 1} failed: ${lastError.message}`,\n );\n\n if (this.isRateLimitError(lastError)) {\n log.error(`[LLMClient:call] Rate limit error, not retrying`);\n throw lastError;\n }\n\n if (attempt < maxRetries && retryWithContext) {\n log.info(`[LLMClient:call] Will retry with error context`);\n currentPrompt =\n `${userPrompt}\\n\\n` +\n `[Previous attempt failed with: ${lastError.message}]\\n` +\n `Please output valid JSON that matches the expected schema.`;\n }\n }\n }\n\n log.error(`[LLMClient:call] All attempts exhausted, throwing error`);\n throw lastError;\n }\n\n private extractFinishReason(\n response: Awaited<ReturnType<ChatOpenAI['invoke']>>,\n ): LLMFinishReason {\n const metadata = response.response_metadata as\n | OpenAIResponseMetadata\n | undefined;\n if (metadata?.finish_reason) {\n const reason = metadata.finish_reason as string;\n if (\n reason === 'stop' ||\n reason === 'length' ||\n reason === 'content_filter' ||\n reason === 'tool_calls'\n ) {\n return reason;\n }\n }\n return null;\n }\n\n /**\n * Default hard timeout for raw LLM invocations. The langchain ChatOpenAI\n * `timeout` option doesn't reliably fire when an HTTP connection\n * half-opens (the response stream can sit open indefinitely), so every\n * raw call gets wrapped in an AbortController-driven deadline. Callers\n * can override by passing their own `signal`.\n */\n private static readonly DEFAULT_RAW_TIMEOUT_MS = 5 * 60 * 1000; // 5 min\n\n /**\n * Wrap a `model.invoke(...)` with start/finish/error logging and a hard\n * abort-driven timeout. Returns whatever the underlying `invoke` returns.\n *\n * The raw paths (`callRaw`, `callRawWithMetadata`, `callWithMessages`)\n * used to be silent — when a connection half-opened, callers sat\n * indefinitely with no feedback. This wrapper makes hangs visible (start\n * + duration logs) and bounded (timeout fires with a clear error).\n */\n private async invokeWithObservability(\n label: string,\n modelToUse: ChatModel,\n langchainMessages: BaseMessageLike[],\n callerSignal?: AbortSignal,\n timeoutMsArg?: number,\n ): Promise<Awaited<ReturnType<ChatModel['invoke']>>> {\n const timeoutMs = timeoutMsArg ?? this.rawTimeoutMs;\n const startedAt = Date.now();\n const controller = new AbortController();\n const timeoutHandle = setTimeout(\n () => controller.abort(new Error(`LLM call timed out after ${timeoutMs}ms`)),\n timeoutMs,\n );\n if (callerSignal) {\n if (callerSignal.aborted) {\n controller.abort(callerSignal.reason);\n } else {\n callerSignal.addEventListener(\n 'abort',\n () => controller.abort(callerSignal.reason),\n { once: true },\n );\n }\n }\n log.info(\n `[LLMClient:${label}] Invoking ${this.provider}/${this.modelName} (timeout=${timeoutMs}ms)`,\n );\n try {\n const response = await modelToUse.invoke(langchainMessages, {\n signal: controller.signal,\n });\n log.info(\n `[LLMClient:${label}] Responded in ${Date.now() - startedAt}ms`,\n );\n return response;\n } catch (err) {\n const elapsed = Date.now() - startedAt;\n const message = err instanceof Error ? err.message : String(err);\n log.error(\n `[LLMClient:${label}] FAILED after ${elapsed}ms: ${message}`,\n );\n throw err;\n } finally {\n clearTimeout(timeoutHandle);\n }\n }\n\n async callRaw(options: {\n systemPrompt: string;\n userPrompt: string;\n maxTokens?: number;\n signal?: AbortSignal;\n }): Promise<string> {\n const response = await this.callRawWithMetadata(options);\n return response.raw;\n }\n\n async callRawWithMetadata(options: {\n systemPrompt: string;\n userPrompt: string;\n maxTokens?: number;\n signal?: AbortSignal;\n }): Promise<Omit<LLMResponse<string>, 'data'> & { raw: string }> {\n const { systemPrompt, userPrompt, maxTokens, signal } = options;\n\n return this.rateLimiter.execute(async () => {\n const modelToUse = maxTokens\n ? this.getModelWithOptions({ maxTokens })\n : this.model;\n\n const messages = [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: this.prepareUserPrompt(userPrompt) },\n ];\n const response = await this.invokeWithObservability(\n 'callRawWithMetadata',\n modelToUse,\n this.provider === 'anthropic'\n ? addCacheControlToSystemMessages(messages)\n : (messages as BaseMessageLike[]),\n signal,\n );\n\n let usage: LLMUsage | null = null;\n if (response.usage_metadata) {\n const usageMeta = response.usage_metadata as {\n input_tokens?: number;\n output_tokens?: number;\n };\n usage = {\n promptTokens: usageMeta.input_tokens || 0,\n completionTokens: usageMeta.output_tokens || 0,\n totalTokens:\n (usageMeta.input_tokens || 0) + (usageMeta.output_tokens || 0),\n };\n\n if (this.tokenTracker) {\n this.tokenTracker.addUsage(\n usage.promptTokens,\n usage.completionTokens,\n { provider: this.provider, ...cacheTokensFromUsageMetadata(usageMeta) },\n );\n }\n }\n\n const finishReason = this.extractFinishReason(response);\n const content =\n typeof response.content === 'string'\n ? response.content\n : JSON.stringify(response.content);\n\n return { raw: content, finishReason, usage };\n });\n }\n\n /**\n * Call the LLM with a structured messages array.\n *\n * Unlike callRawWithMetadata (which takes systemPrompt + userPrompt strings),\n * this accepts a full conversation history with proper role separation.\n * This enables:\n * - Anthropic prompt caching on message boundaries (not just system prompt)\n * - Proper tool_use/tool_result role handling across providers\n * - Reduced token waste from string concatenation\n *\n * All providers support the messages format:\n * - Anthropic: native messages API with cache_control\n * - DeepSeek: OpenAI-compatible messages via ChatOpenAI\n * - OpenRouter: OpenAI-compatible messages via ChatOpenAI\n */\n async callWithMessages(options: {\n messages: Array<{ role: string; content: string }>;\n maxTokens?: number;\n signal?: AbortSignal;\n }): Promise<Omit<LLMResponse<string>, 'data'> & { raw: string }> {\n const { messages, maxTokens, signal } = options;\n\n return this.rateLimiter.execute(async () => {\n const modelToUse = maxTokens\n ? this.getModelWithOptions({ maxTokens })\n : this.model;\n\n const langchainMessages = this.provider === 'anthropic'\n ? addCacheControlToSystemMessages(messages)\n : (messages as BaseMessageLike[]);\n\n const response = await this.invokeWithObservability(\n 'callWithMessages',\n modelToUse,\n langchainMessages,\n signal,\n );\n\n let usage: LLMUsage | null = null;\n if (response.usage_metadata) {\n const usageMeta = response.usage_metadata as {\n input_tokens?: number;\n output_tokens?: number;\n };\n usage = {\n promptTokens: usageMeta.input_tokens || 0,\n completionTokens: usageMeta.output_tokens || 0,\n totalTokens:\n (usageMeta.input_tokens || 0) + (usageMeta.output_tokens || 0),\n };\n\n if (this.tokenTracker) {\n this.tokenTracker.addUsage(\n usage.promptTokens,\n usage.completionTokens,\n { provider: this.provider, ...cacheTokensFromUsageMetadata(usageMeta) },\n );\n }\n }\n\n const finishReason = this.extractFinishReason(response);\n const content =\n typeof response.content === 'string'\n ? response.content\n : JSON.stringify(response.content);\n\n return { raw: content, finishReason, usage };\n });\n }\n\n /**\n * Tool-calling chat-completion call that speaks the OpenAI wire format\n * directly via `fetch`, bypassing LangChain's `ChatOpenAI` converter.\n *\n * MOTIVATION: LangChain's `convertMessagesToCompletionsMessageParams`\n * silently drops every `additional_kwargs` field except `function_call`\n * and `tool_calls`. DeepSeek V4 thinking-mode requires\n * `reasoning_content` to be echoed back on assistant turns that\n * triggered tool_calls; LangChain's converter strips it, the next\n * round-trip fails with \"400 The reasoning_content in the thinking\n * mode must be passed back to the API.\" This method preserves every\n * assistant field verbatim across round-trips.\n *\n * Supported providers: any OpenAI-compatible endpoint (openai,\n * deepseek, openrouter, kimi, orbgen). Anthropic uses a different\n * wire format and is intentionally not supported here — use\n * `callWithMessages` for Anthropic.\n *\n * Defaults `parallel_tool_calls: false` — sequential tool dispatch is\n * the protocol-safe baseline. Multi-tool-call assistant messages\n * trigger DeepSeek's \"insufficient tool messages\" 400 error.\n */\n async callWithTools(options: {\n messages: ReadonlyArray<ChatCompletionMessage>;\n tools: ReadonlyArray<ChatCompletionToolDef>;\n maxTokens?: number;\n parallelToolCalls?: boolean;\n signal?: AbortSignal;\n /** Per-call deadline (ms). Defaults to `rawTimeoutMs` (5 min unless the\n * client was constructed with a lower value). Bounds the fetch so a\n * hung connection can't run past the caller's own wall-clock cap. */\n timeoutMs?: number;\n }): Promise<{\n message: ChatCompletionMessage;\n finishReason: string;\n usage: LLMUsage | null;\n }> {\n if (this.provider === 'anthropic') {\n throw new Error(\n 'LLMClient.callWithTools: anthropic provider is not supported; use callWithMessages instead',\n );\n }\n return this.rateLimiter.execute(async () => {\n const baseUrl = (this.providerConfig.baseUrl ?? 'https://api.openai.com/v1').replace(/\\/$/, '');\n const url = `${baseUrl}/chat/completions`;\n\n const body: { [key: string]: unknown } = {\n model: this.modelName,\n messages: options.messages,\n parallel_tool_calls: options.parallelToolCalls ?? false,\n temperature: this.temperature,\n };\n if (options.tools.length > 0) body['tools'] = options.tools;\n if (options.maxTokens !== undefined) body['max_tokens'] = options.maxTokens;\n // OpenRouter requires explicit opt-in for `reasoning` (chain-of-thought)\n // on models that support it (Gemma 4, etc.). DeepSeek's native API\n // returns `reasoning_content` without an opt-in; OpenRouter returns\n // the field as `reasoning`. We send the flag always for openrouter —\n // models that don't support it ignore it. The response-side normaliser\n // below maps `reasoning` → `reasoning_content` so downstream\n // consumers see one canonical field name.\n if (this.provider === 'openrouter') {\n body['reasoning'] = { enabled: true };\n // Return authoritative usage accounting (real cost + cached-token split).\n body['usage'] = { include: true };\n }\n\n const startedAt = Date.now();\n log.info(\n `[LLMClient:callWithTools] Invoking ${this.provider}/${this.modelName} (tools=${options.tools.length}, messages=${options.messages.length})`,\n );\n\n // Per-call deadline. callWithTools previously honored only a\n // caller-supplied signal, so when runToolLoop passed none a hung\n // connection ran until the caller's external wall-clock cap (e.g. a\n // 300s battery race), with the subagent producing zero metrics. Drive\n // an AbortController off `timeoutMs ?? rawTimeoutMs` and merge the\n // caller's signal so a hang aborts in-process, fast.\n const effectiveTimeoutMs = options.timeoutMs ?? this.rawTimeoutMs;\n const controller = new AbortController();\n const timeoutHandle = setTimeout(\n () => controller.abort(new Error(`LLMClient.callWithTools timed out after ${effectiveTimeoutMs}ms`)),\n effectiveTimeoutMs,\n );\n const callerSignal = options.signal;\n if (callerSignal) {\n if (callerSignal.aborted) controller.abort(callerSignal.reason);\n else callerSignal.addEventListener('abort', () => controller.abort(callerSignal.reason), { once: true });\n }\n const fetchInit: RequestInit = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.providerConfig.apiKey}`,\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n };\n try {\n const response = await fetch(url, fetchInit);\n\n if (!response.ok) {\n const errText = await response.text().catch(() => '<no body>');\n const elapsed = Date.now() - startedAt;\n log.error(\n `[LLMClient:callWithTools] FAILED after ${elapsed}ms (${response.status} ${response.statusText}): ${errText}`,\n );\n throw new Error(\n `LLMClient.callWithTools: ${this.provider} returned ${response.status} ${response.statusText}: ${errText}`,\n );\n }\n\n const raw = await response.text();\n const parsed = parseChatCompletionResponse(raw);\n const choice = parsed.choices[0];\n if (!choice) {\n throw new Error('LLMClient.callWithTools: no choices in response');\n }\n // Normalise OpenRouter's `reasoning` field to DeepSeek's\n // `reasoning_content` so downstream code (rabit's `fromWireMessage`\n // at runtime/services/tool-loop.ts:103) reads one canonical field\n // for chain-of-thought regardless of provider. Cast through\n // `as { reasoning?: string }` because `ChatCompletionMessage`'s\n // type doesn't declare the off-spec `reasoning` field.\n const msgWithReasoning = choice.message as ChatCompletionMessage & { reasoning?: string };\n if (\n msgWithReasoning.reasoning !== undefined &&\n choice.message.reasoning_content === undefined\n ) {\n choice.message.reasoning_content = msgWithReasoning.reasoning;\n }\n\n let usage: LLMUsage | null = null;\n if (parsed.usage) {\n usage = {\n promptTokens: parsed.usage.prompt_tokens,\n completionTokens: parsed.usage.completion_tokens,\n totalTokens: parsed.usage.total_tokens,\n };\n if (this.tokenTracker) {\n const rawUsage = parsed.usage as {\n prompt_tokens_details?: { cached_tokens?: number };\n cost?: number;\n };\n const cachedTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? 0;\n this.tokenTracker.addUsage(usage.promptTokens, usage.completionTokens, {\n provider: this.provider,\n cachedPromptTokens: cachedTokens,\n // OpenRouter returns the real, routing+cache-adjusted charge here.\n ...(typeof rawUsage.cost === 'number' ? { costUSD: rawUsage.cost } : {}),\n });\n }\n }\n\n log.info(\n `[LLMClient:callWithTools] Responded in ${Date.now() - startedAt}ms (prompt=${usage?.promptTokens ?? 0}, completion=${usage?.completionTokens ?? 0}, tool_calls=${choice.message.tool_calls?.length ?? 0})`,\n );\n\n return {\n message: choice.message,\n finishReason: choice.finish_reason,\n usage,\n };\n } finally {\n clearTimeout(timeoutHandle);\n }\n });\n }\n\n /**\n * Stream a raw text response as an async iterator of content chunks.\n * Uses the underlying LangChain model's .stream() method.\n *\n * @param options - System prompt plus full message history\n * @yields LLMStreamChunk with content deltas and a done flag\n */\n async *streamRaw(options: LLMStreamOptions): AsyncGenerator<LLMStreamChunk> {\n const { messages, maxTokens, temperature } = options;\n\n const modelToUse = (maxTokens || temperature !== undefined)\n ? this.getModelWithOptions({ maxTokens, temperature })\n : this.model;\n\n const langchainMessages = this.provider === 'anthropic'\n ? addCacheControlToSystemMessages(messages)\n : messages;\n\n const stream = await modelToUse.stream(langchainMessages);\n\n for await (const chunk of stream) {\n const content = typeof chunk.content === 'string'\n ? chunk.content\n : Array.isArray(chunk.content)\n ? chunk.content\n .filter((c): c is { type: 'text'; text: string } => typeof c === 'object' && c !== null && 'text' in c)\n .map((c) => c.text)\n .join('')\n : '';\n\n if (content) {\n yield { content, done: false };\n }\n }\n\n yield { content: '', done: true };\n }\n\n private isRateLimitError(error: Error): boolean {\n const message = error.message.toLowerCase();\n return (\n message.includes('rate limit') ||\n message.includes('429') ||\n message.includes('quota exceeded')\n );\n }\n\n // ==========================================================================\n // Anthropic Cache Control Support\n // ==========================================================================\n\n async callWithCache<T>(\n options: CacheAwareLLMCallOptions<T>,\n ): Promise<LLMResponse<T>> {\n const {\n systemPrompt,\n userPrompt,\n systemBlocks,\n userBlocks,\n schema,\n maxRetries = 2,\n maxTokens,\n skipSchemaValidation = false,\n temperature,\n rawText = false,\n } = options;\n\n if (this.provider !== 'anthropic') {\n log.info(\n `[LLMClient:callWithCache] Provider ${this.provider} doesn't support caching, using regular call`,\n );\n return this.callWithMetadata(options);\n }\n\n const cacheableCount =\n (systemBlocks || []).filter((b) => b.cache_control).length +\n (userBlocks || []).filter((b) => b.cache_control).length;\n log.info(\n `[LLMClient:callWithCache] ${cacheableCount} cacheable block(s)`,\n );\n\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n log.info(\n `[LLMClient:callWithCache] Attempt ${attempt + 1}/${maxRetries + 1}...`,\n );\n\n const result = await this.rateLimiter.execute(async () => {\n const anthropic = new Anthropic();\n\n const systemContent =\n systemBlocks && systemBlocks.length > 0\n ? systemBlocks.map((b) => ({\n type: 'text' as const,\n text: b.text,\n ...(b.cache_control\n ? { cache_control: b.cache_control }\n : {}),\n }))\n : systemPrompt\n ? [{ type: 'text' as const, text: systemPrompt }]\n : [];\n\n const userContent =\n userBlocks && userBlocks.length > 0\n ? userBlocks.map((b) => ({\n type: 'text' as const,\n text: b.text,\n ...(b.cache_control\n ? { cache_control: b.cache_control }\n : {}),\n }))\n : userPrompt\n ? [{ type: 'text' as const, text: userPrompt }]\n : [];\n\n const response = await anthropic.messages.create({\n model: this.modelName,\n max_tokens: maxTokens || 8192,\n temperature: temperature ?? 0,\n system: systemContent,\n messages: [{ role: 'user', content: userContent }],\n });\n\n const textContent = response.content.find((c) => c.type === 'text');\n const content =\n textContent && 'text' in textContent ? textContent.text : '';\n\n const apiUsage = response.usage as {\n input_tokens: number;\n output_tokens: number;\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n };\n\n const cacheRead = apiUsage.cache_read_input_tokens || 0;\n const cacheCreation = apiUsage.cache_creation_input_tokens || 0;\n\n if (cacheCreation > 0) {\n log.info(\n `[LLMClient:callWithCache] Cache WRITE: ${cacheCreation} tokens`,\n );\n }\n if (cacheRead > 0) {\n const savingsPercent = Math.round(\n (cacheRead / (cacheRead + apiUsage.input_tokens)) * 100,\n );\n log.info(\n `[LLMClient:callWithCache] Cache HIT: ${cacheRead} tokens (~${savingsPercent}% of prompt)`,\n );\n }\n if (cacheCreation === 0 && cacheRead === 0) {\n log.info(\n `[LLMClient:callWithCache] No caching: ${apiUsage.input_tokens} input tokens`,\n );\n }\n\n const usage: LLMUsage = {\n promptTokens: apiUsage.input_tokens,\n completionTokens: apiUsage.output_tokens,\n totalTokens: apiUsage.input_tokens + apiUsage.output_tokens,\n };\n\n if (this.tokenTracker) {\n // Anthropic reports input_tokens as the UNCACHED count; cache reads\n // and writes are separate. Pass the true total so the tracker prices\n // each bucket at its own rate.\n this.tokenTracker.addUsage(\n apiUsage.input_tokens + cacheRead + cacheCreation,\n usage.completionTokens,\n { provider: this.provider, cachedPromptTokens: cacheRead, cacheWriteTokens: cacheCreation },\n );\n }\n\n const finishReason =\n response.stop_reason === 'end_turn'\n ? 'stop'\n : response.stop_reason;\n\n return {\n content,\n finishReason: finishReason as LLMFinishReason,\n usage,\n };\n });\n\n let parsed: T;\n if (rawText) {\n // rawText mode: caller expects T = string; content is already a string\n parsed = asGeneric<T>(result.content);\n } else if (skipSchemaValidation) {\n parsed = parseJsonResponse(result.content, undefined) as T;\n } else {\n parsed = parseJsonResponse(result.content, schema);\n }\n\n return {\n data: parsed,\n raw: result.content,\n finishReason: result.finishReason,\n usage: result.usage,\n };\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n log.error(\n `[LLMClient:callWithCache] Attempt ${attempt + 1} failed: ${lastError.message}`,\n );\n\n if (this.isRateLimitError(lastError)) {\n throw lastError;\n }\n }\n }\n\n throw lastError;\n }\n\n static cacheableBlock(text: string, cache = true): CacheableBlock {\n return cache\n ? { type: 'text', text, cache_control: { type: 'ephemeral' } }\n : { type: 'text', text };\n }\n}\n\n// ============================================================================\n// Singleton Instances\n// ============================================================================\n\nconst sharedClients: Partial<Record<LLMProvider, LLMClient>> = {};\n\nexport function getSharedLLMClient(options?: LLMClientOptions): LLMClient {\n const provider = options?.provider || 'openai';\n if (!sharedClients[provider]) {\n sharedClients[provider] = new LLMClient(options);\n }\n return sharedClients[provider]!;\n}\n\nexport function resetSharedLLMClient(provider?: LLMProvider): void {\n if (provider) {\n delete sharedClients[provider];\n } else {\n for (const key of Object.keys(sharedClients) as LLMProvider[]) {\n delete sharedClients[key];\n }\n }\n}\n\n// ============================================================================\n// Provider Detection\n// ============================================================================\n\nexport function getAvailableProvider(): LLMProvider {\n if (process.env.ANTHROPIC_API_KEY) return 'anthropic';\n if (process.env.DEEPSEEK_API_KEY) return 'deepseek';\n if (process.env.KIMI_API_KEY) return 'kimi';\n if (process.env.OPENAI_API_KEY) return 'openai';\n throw new Error(\n 'No LLM API key found. Please set ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, or KIMI_API_KEY.',\n );\n}\n\nexport function isProviderAvailable(provider: LLMProvider): boolean {\n switch (provider) {\n case 'openai':\n return !!process.env.OPENAI_API_KEY;\n case 'deepseek':\n return !!process.env.DEEPSEEK_API_KEY;\n case 'anthropic':\n return !!process.env.ANTHROPIC_API_KEY;\n case 'kimi':\n return !!process.env.KIMI_API_KEY;\n case 'openrouter':\n return !!process.env.OPEN_ROUTER_API_KEY;\n case 'orbgen':\n return !!process.env.ORBGEN_URL;\n case 'masar':\n return !!process.env.MASAR_SUBAGENT_URL;\n default:\n return false;\n }\n}\n\n// ============================================================================\n// Convenience Functions\n// ============================================================================\n\n/**\n * Create an LLM client optimized for requirements analysis.\n *\n * Uses lower temperature (0.3) for more deterministic output.\n * Defaults to GPT-5.1 for OpenAI or DeepSeek Chat.\n *\n * @param {Partial<LLMClientOptions>} [options] - Optional client configuration\n * @returns {LLMClient} Configured LLM client\n */\nexport function createRequirementsClient(\n options?: Partial<LLMClientOptions>,\n): LLMClient {\n const provider = options?.provider || getAvailableProvider();\n const defaultModel =\n provider === 'deepseek' ? DEEPSEEK_MODELS.CHAT : OPENAI_MODELS.GPT_5_1;\n return new LLMClient({\n provider,\n model: defaultModel,\n temperature: 0.3,\n ...options,\n });\n}\n\n/**\n * Create an LLM client optimized for creative tasks.\n *\n * Uses higher temperature (0.7) for more varied output.\n * Defaults to GPT-4o or DeepSeek Reasoner.\n *\n * @param {Partial<LLMClientOptions>} [options] - Optional client configuration\n * @returns {LLMClient} Configured LLM client\n */\nexport function createCreativeClient(\n options?: Partial<LLMClientOptions>,\n): LLMClient {\n const provider = options?.provider || getAvailableProvider();\n const defaultModel =\n provider === 'deepseek' ? DEEPSEEK_MODELS.REASONER : OPENAI_MODELS.GPT4O;\n return new LLMClient({\n provider,\n model: defaultModel,\n temperature: 0.7,\n ...options,\n });\n}\n\n/**\n * Create an LLM client optimized for code fixing.\n *\n * Uses low temperature (0.2) for precise, deterministic fixes.\n * Defaults to GPT-4o Mini or DeepSeek Chat for cost efficiency.\n *\n * @param {Partial<LLMClientOptions>} [options] - Optional client configuration\n * @returns {LLMClient} Configured LLM client\n */\nexport function createFixClient(\n options?: Partial<LLMClientOptions>,\n): LLMClient {\n const provider = options?.provider || getAvailableProvider();\n const defaultModel =\n provider === 'deepseek'\n ? DEEPSEEK_MODELS.CHAT\n : OPENAI_MODELS.GPT4O_MINI;\n return new LLMClient({\n provider,\n model: defaultModel,\n temperature: 0.2,\n ...options,\n });\n}\n\n/**\n * Create a DeepSeek LLM client.\n *\n * @param {Partial<Omit<LLMClientOptions, 'provider'>>} [options] - Optional client configuration\n * @returns {LLMClient} Configured DeepSeek client\n */\nexport function createDeepSeekClient(\n options?: Partial<Omit<LLMClientOptions, 'provider'>>,\n): LLMClient {\n return new LLMClient({\n provider: 'deepseek',\n model: DEEPSEEK_MODELS.CHAT,\n ...options,\n });\n}\n\n/**\n * Create an OpenAI LLM client.\n *\n * @param {Partial<Omit<LLMClientOptions, 'provider'>>} [options] - Optional client configuration\n * @returns {LLMClient} Configured OpenAI client\n */\nexport function createOpenAIClient(\n options?: Partial<Omit<LLMClientOptions, 'provider'>>,\n): LLMClient {\n return new LLMClient({\n provider: 'openai',\n model: OPENAI_MODELS.GPT4O,\n ...options,\n });\n}\n\n/**\n * Create an Anthropic LLM client.\n *\n * @param {Partial<Omit<LLMClientOptions, 'provider'>>} [options] - Optional client configuration\n * @returns {LLMClient} Configured Anthropic client\n */\nexport function createAnthropicClient(\n options?: Partial<Omit<LLMClientOptions, 'provider'>>,\n): LLMClient {\n return new LLMClient({\n provider: 'anthropic',\n model: ANTHROPIC_MODELS.CLAUDE_SONNET_4_5,\n ...options,\n });\n}\n\n/**\n * Create a Kimi LLM client.\n *\n * @param {Partial<Omit<LLMClientOptions, 'provider'>>} [options] - Optional client configuration\n * @returns {LLMClient} Configured Kimi client\n */\nexport function createKimiClient(\n options?: Partial<Omit<LLMClientOptions, 'provider'>>,\n): LLMClient {\n return new LLMClient({\n provider: 'kimi',\n model: KIMI_MODELS.K2_5,\n ...options,\n });\n}\n\n/**\n * Create an OpenRouter LLM client.\n *\n * @param {Partial<Omit<LLMClientOptions, 'provider'>>} [options] - Optional client configuration\n * @returns {LLMClient} Configured OpenRouter client\n */\nexport function createOpenRouterClient(\n options?: Partial<Omit<LLMClientOptions, 'provider'>>,\n): LLMClient {\n return new LLMClient({\n provider: 'openrouter',\n model: OPENROUTER_MODELS.QWEN_2_5_72B,\n ...options,\n });\n}\n\n/**\n * Create a Zhipu (GLM) LLM client via OpenRouter.\n *\n * @param {Partial<Omit<LLMClientOptions, 'provider'>>} [options] - Optional client configuration\n * @returns {LLMClient} Configured Zhipu client\n */\nexport function createZhipuClient(\n options?: Partial<Omit<LLMClientOptions, 'provider'>>,\n): LLMClient {\n return new LLMClient({\n provider: 'openrouter',\n model: OPENROUTER_MODELS.GLM_4_7,\n ...options,\n });\n}\n","/**\n * OpenAI Chat Completions wire-format types used by `LLMClient.callWithTools`.\n *\n * These mirror the public OpenAI Chat Completions API spec, which is also\n * the protocol every OpenAI-compatible provider (DeepSeek, OpenRouter,\n * Kimi, OrbGen, etc.) implements. The types are intentionally faithful\n * to the wire format — when the LLM emits a `reasoning_content` field\n * (DeepSeek V4 thinking mode), it's preserved verbatim and echoed back\n * on the next round-trip.\n */\n\nimport type { JsonSchema } from '@almadar/core';\n\n// ============================================================================\n// Tool definitions sent to the API\n// ============================================================================\n\nexport interface ChatCompletionToolDef {\n type: 'function';\n function: {\n name: string;\n description: string;\n /** JSON Schema describing the tool's parameters. Sent verbatim to the\n * provider as the tool-call `function.parameters` field. */\n parameters: JsonSchema;\n };\n}\n\n// ============================================================================\n// Message shape (used in both request and response)\n// ============================================================================\n\nexport type ChatCompletionRole = 'system' | 'user' | 'assistant' | 'tool';\n\nexport interface ChatCompletionToolCall {\n id: string;\n type: 'function';\n function: { name: string; arguments: string };\n}\n\nexport interface ChatCompletionMessage {\n role: ChatCompletionRole;\n /** Null is valid (assistant-only) when the message exists purely to carry `tool_calls`. */\n content: string | null;\n /** Present on assistant turns that called one or more tools. */\n tool_calls?: ChatCompletionToolCall[];\n /** Present on tool-role messages — matches `tool_calls[*].id` of the preceding assistant turn. */\n tool_call_id?: string;\n /**\n * DeepSeek V4 thinking-mode chain-of-thought string. Must be echoed\n * back on the next round-trip when the assistant turn triggered\n * tool_calls — that's the protocol contract that LangChain's\n * ChatOpenAI converter breaks.\n */\n reasoning_content?: string;\n}\n\n// ============================================================================\n// Response shape\n// ============================================================================\n\nexport interface ChatCompletionChoice {\n index: number;\n message: ChatCompletionMessage;\n finish_reason: string;\n}\n\nexport interface ChatCompletionUsage {\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n}\n\nexport interface ChatCompletionResponse {\n choices: ChatCompletionChoice[];\n usage?: ChatCompletionUsage;\n}\n\n// ============================================================================\n// Response parsing — strict type narrowing without `as unknown as`\n// ============================================================================\n\nexport function parseChatCompletionResponse(raw: string): ChatCompletionResponse {\n let json: unknown;\n try {\n json = JSON.parse(raw);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n throw new Error(\n `parseChatCompletionResponse: not valid JSON (${reason}): ${raw.slice(0, 400)}`,\n );\n }\n if (!isChatCompletionResponse(json)) {\n throw new Error(\n `parseChatCompletionResponse: response did not match expected shape: ${raw.slice(0, 400)}`,\n );\n }\n return json;\n}\n\nfunction isChatCompletionResponse(value: unknown): value is ChatCompletionResponse {\n if (value === null || typeof value !== 'object') return false;\n const obj = value as { choices?: unknown };\n if (!Array.isArray(obj.choices)) return false;\n for (const c of obj.choices) {\n if (!isChatCompletionChoice(c)) return false;\n }\n return true;\n}\n\nfunction isChatCompletionChoice(value: unknown): value is ChatCompletionChoice {\n if (value === null || typeof value !== 'object') return false;\n const c = value as { message?: unknown; finish_reason?: unknown; index?: unknown };\n if (typeof c.finish_reason !== 'string') return false;\n if (typeof c.index !== 'number') return false;\n if (!isChatCompletionMessage(c.message)) return false;\n return true;\n}\n\nfunction isChatCompletionMessage(value: unknown): value is ChatCompletionMessage {\n if (value === null || typeof value !== 'object') return false;\n const m = value as { role?: unknown; content?: unknown };\n if (typeof m.role !== 'string') return false;\n if (m.content !== null && typeof m.content !== 'string') return false;\n return true;\n}\n"],"mappings":"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","names":[]}
package/dist/client.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  getSharedLLMClient,
19
19
  isProviderAvailable,
20
20
  resetSharedLLMClient
21
- } from "./chunk-G2OE5TBE.js";
21
+ } from "./chunk-V4O6E2VI.js";
22
22
  import "./chunk-P4VCT25B.js";
23
23
  import "./chunk-SXSP6M24.js";
24
24
  export {
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  isProviderAvailable,
20
20
  parseChatCompletionResponse,
21
21
  resetSharedLLMClient
22
- } from "./chunk-G2OE5TBE.js";
22
+ } from "./chunk-V4O6E2VI.js";
23
23
  import {
24
24
  autoCloseJson,
25
25
  extractJsonFromText,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/llm",
3
- "version": "2.22.0",
3
+ "version": "2.24.0",
4
4
  "description": "Multi-provider LLM client with rate limiting, token tracking, structured outputs, and continuation handling",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -37,7 +37,8 @@
37
37
  "@langchain/core": "^1.1.32",
38
38
  "@langchain/openai": "^1.2.13",
39
39
  "openai": "^6.18.0",
40
- "zod": "^3.22.0"
40
+ "zod": "^3.22.0",
41
+ "@almadar/logger": "^1.3.0"
41
42
  },
42
43
  "peerDependencies": {
43
44
  "@almadar/core": ">=8.5.0"
package/src/client.ts CHANGED
@@ -28,6 +28,11 @@ import {
28
28
  type ChatCompletionMessage,
29
29
  type ChatCompletionToolDef,
30
30
  } from './tool-call-types.js';
31
+ import { createLogger } from '@almadar/logger';
32
+
33
+ // Structured logger so callers (e.g. the orb CLI's ink TUI) can silence this
34
+ // chatter via `setRuntimeNamespaceFilter('')`; raw console.log can't be muted.
35
+ const log = createLogger('almadar:llm:client');
31
36
 
32
37
  // ============================================================================
33
38
  // Local type helpers (avoid Record<string, unknown> and unsafe casts)
@@ -216,6 +221,20 @@ export interface LLMStreamChunk {
216
221
  // Provider Configuration
217
222
  // ============================================================================
218
223
 
224
+ /**
225
+ * Canonical base URL per OpenAI-compatible provider — the single source of
226
+ * truth. Used both by the env-mode factories below and by the constructor's
227
+ * back-fill, so an explicit `providerConfig` that omits `baseUrl` (e.g. the
228
+ * CLI's `~/.almadar` account, which only stores apiKey) is sent to the
229
+ * PROVIDER's endpoint, not api.openai.com. openai/anthropic intentionally have
230
+ * none (their SDK defaults are correct); orbgen/masar derive theirs from env.
231
+ */
232
+ const PROVIDER_BASE_URLS: Partial<Record<LLMProvider, string>> = {
233
+ deepseek: 'https://api.deepseek.com/v1',
234
+ kimi: 'https://api.moonshot.ai/v1',
235
+ openrouter: 'https://openrouter.ai/api/v1',
236
+ };
237
+
219
238
  const PROVIDER_CONFIGS: Record<LLMProvider, () => ProviderConfig> = {
220
239
  openai: () => {
221
240
  const apiKey = process.env.OPENAI_API_KEY;
@@ -237,7 +256,7 @@ const PROVIDER_CONFIGS: Record<LLMProvider, () => ProviderConfig> = {
237
256
  }
238
257
  return {
239
258
  apiKey,
240
- baseUrl: 'https://api.deepseek.com/v1',
259
+ baseUrl: PROVIDER_BASE_URLS.deepseek,
241
260
  defaultModel: 'deepseek-v4-flash',
242
261
  };
243
262
  },
@@ -265,7 +284,7 @@ const PROVIDER_CONFIGS: Record<LLMProvider, () => ProviderConfig> = {
265
284
  }
266
285
  return {
267
286
  apiKey,
268
- baseUrl: 'https://api.moonshot.ai/v1',
287
+ baseUrl: PROVIDER_BASE_URLS.kimi,
269
288
  defaultModel: 'kimi-k2.5',
270
289
  };
271
290
  },
@@ -279,7 +298,7 @@ const PROVIDER_CONFIGS: Record<LLMProvider, () => ProviderConfig> = {
279
298
  }
280
299
  return {
281
300
  apiKey,
282
- baseUrl: 'https://openrouter.ai/api/v1',
301
+ baseUrl: PROVIDER_BASE_URLS.openrouter,
283
302
  defaultModel: 'qwen/qwen-2.5-72b-instruct', // Default to Qwen 2.5
284
303
  };
285
304
  },
@@ -398,15 +417,25 @@ export class LLMClient {
398
417
  this.streaming = options.streaming ?? false;
399
418
 
400
419
  this.providerConfig = options.providerConfig ?? PROVIDER_CONFIGS[this.provider]();
420
+ // An explicit providerConfig (e.g. from the CLI's ~/.almadar account, which
421
+ // stores only apiKey) may omit baseUrl. Back-fill the PROVIDER's default so
422
+ // the request isn't sent to api.openai.com — the fallback that createModel()
423
+ // and callWithTools() use when baseUrl is undefined. Env-mode already sets
424
+ // the right baseUrl via PROVIDER_CONFIGS, and openai/anthropic correctly
425
+ // stay undefined (no entry in PROVIDER_BASE_URLS).
426
+ const defaultBaseUrl = PROVIDER_BASE_URLS[this.provider];
427
+ if (this.providerConfig.baseUrl === undefined && defaultBaseUrl) {
428
+ this.providerConfig = { ...this.providerConfig, baseUrl: defaultBaseUrl };
429
+ }
401
430
  this.modelName = options.model || this.providerConfig.defaultModel;
402
431
  this.rawTimeoutMs = options.rawTimeoutMs ?? LLMClient.DEFAULT_RAW_TIMEOUT_MS;
403
432
 
404
433
  const keyPreview = this.providerConfig.apiKey.slice(-4);
405
- console.log(
434
+ log.info(
406
435
  `[LLMClient] Provider: ${this.provider}, Model: ${this.modelName}, Key: ****${keyPreview}`,
407
436
  );
408
437
  if (this.providerConfig.baseUrl) {
409
- console.log(
438
+ log.info(
410
439
  `[LLMClient] Using custom base URL: ${this.providerConfig.baseUrl}`,
411
440
  );
412
441
  }
@@ -464,7 +493,7 @@ export class LLMClient {
464
493
  const outputTokens = usage.output_tokens ?? 0;
465
494
 
466
495
  if (cacheCreated > 0) {
467
- console.log(
496
+ log.info(
468
497
  `[LLMClient:Anthropic] Cache WRITE: ${cacheCreated} tokens cached`,
469
498
  );
470
499
  }
@@ -472,17 +501,17 @@ export class LLMClient {
472
501
  const savingsPercent = Math.round(
473
502
  (cacheRead / (cacheRead + inputTokens)) * 100,
474
503
  );
475
- console.log(
504
+ log.info(
476
505
  `[LLMClient:Anthropic] Cache HIT: ${cacheRead} tokens (~${savingsPercent}% of prompt)`,
477
506
  );
478
507
  }
479
508
  if (cacheCreated === 0 && cacheRead === 0 && inputTokens > 0) {
480
509
  if (inputTokens < 500) {
481
- console.log(
510
+ log.info(
482
511
  `[LLMClient:Anthropic] ${inputTokens} input, ${outputTokens} output tokens (likely cached)`,
483
512
  );
484
513
  } else {
485
- console.log(
514
+ log.info(
486
515
  `[LLMClient:Anthropic] ${inputTokens} input, ${outputTokens} output tokens`,
487
516
  );
488
517
  }
@@ -608,23 +637,23 @@ export class LLMClient {
608
637
  let currentPrompt = userPrompt;
609
638
  let lastError: Error | null = null;
610
639
 
611
- console.log(
640
+ log.info(
612
641
  `[LLMClient:call] Starting call to ${this.provider}/${this.modelName}`,
613
642
  );
614
- console.log(`[LLMClient:call] Prompt length: ${userPrompt.length} chars`);
643
+ log.info(`[LLMClient:call] Prompt length: ${userPrompt.length} chars`);
615
644
  if (maxTokens) {
616
- console.log(`[LLMClient:call] Max tokens: ${maxTokens}`);
645
+ log.info(`[LLMClient:call] Max tokens: ${maxTokens}`);
617
646
  }
618
647
 
619
648
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
620
649
  try {
621
- console.log(
650
+ log.info(
622
651
  `[LLMClient:call] Attempt ${attempt + 1}/${maxRetries + 1}...`,
623
652
  );
624
653
  const attemptStartTime = Date.now();
625
654
 
626
655
  const result = await this.rateLimiter.execute(async () => {
627
- console.log(`[LLMClient:call] Invoking model...`);
656
+ log.info(`[LLMClient:call] Invoking model...`);
628
657
  const invokeStartTime = Date.now();
629
658
 
630
659
  const modelToUse =
@@ -642,7 +671,7 @@ export class LLMClient {
642
671
  : messages,
643
672
  );
644
673
 
645
- console.log(
674
+ log.info(
646
675
  `[LLMClient:call] Model responded in ${Date.now() - invokeStartTime}ms`,
647
676
  );
648
677
 
@@ -659,7 +688,7 @@ export class LLMClient {
659
688
  (usageMeta.input_tokens || 0) +
660
689
  (usageMeta.output_tokens || 0),
661
690
  };
662
- console.log(
691
+ log.info(
663
692
  `[LLMClient:call] Tokens used: ${usage.promptTokens} in, ${usage.completionTokens} out`,
664
693
  );
665
694
 
@@ -674,7 +703,7 @@ export class LLMClient {
674
703
 
675
704
  const finishReason = this.extractFinishReason(response);
676
705
  if (finishReason === 'length') {
677
- console.warn(
706
+ log.warn(
678
707
  `[LLMClient:call] Response truncated (finish_reason=length)`,
679
708
  );
680
709
  }
@@ -684,21 +713,21 @@ export class LLMClient {
684
713
  ? response.content
685
714
  : JSON.stringify(response.content);
686
715
 
687
- console.log(
716
+ log.info(
688
717
  `[LLMClient:call] Response length: ${content.length} chars, finish_reason: ${finishReason}`,
689
718
  );
690
719
 
691
720
  return { content, finishReason, usage };
692
721
  });
693
722
 
694
- console.log(
723
+ log.info(
695
724
  `[LLMClient:call] Attempt ${attempt + 1} completed in ${Date.now() - attemptStartTime}ms, parsing response...`,
696
725
  );
697
726
 
698
727
  const parsed = skipSchemaValidation
699
728
  ? (parseJsonResponse(result.content, undefined) as T)
700
729
  : parseJsonResponse(result.content, schema);
701
- console.log(
730
+ log.info(
702
731
  `[LLMClient:call] Response parsed successfully${skipSchemaValidation ? ' (schema validation skipped)' : ''}`,
703
732
  );
704
733
 
@@ -710,18 +739,17 @@ export class LLMClient {
710
739
  };
711
740
  } catch (error) {
712
741
  lastError = error instanceof Error ? error : new Error(String(error));
713
- console.error(
714
- `[LLMClient:call] Attempt ${attempt + 1} failed:`,
715
- lastError.message,
742
+ log.error(
743
+ `[LLMClient:call] Attempt ${attempt + 1} failed: ${lastError.message}`,
716
744
  );
717
745
 
718
746
  if (this.isRateLimitError(lastError)) {
719
- console.error(`[LLMClient:call] Rate limit error, not retrying`);
747
+ log.error(`[LLMClient:call] Rate limit error, not retrying`);
720
748
  throw lastError;
721
749
  }
722
750
 
723
751
  if (attempt < maxRetries && retryWithContext) {
724
- console.log(`[LLMClient:call] Will retry with error context`);
752
+ log.info(`[LLMClient:call] Will retry with error context`);
725
753
  currentPrompt =
726
754
  `${userPrompt}\n\n` +
727
755
  `[Previous attempt failed with: ${lastError.message}]\n` +
@@ -730,7 +758,7 @@ export class LLMClient {
730
758
  }
731
759
  }
732
760
 
733
- console.error(`[LLMClient:call] All attempts exhausted, throwing error`);
761
+ log.error(`[LLMClient:call] All attempts exhausted, throwing error`);
734
762
  throw lastError;
735
763
  }
736
764
 
@@ -797,21 +825,21 @@ export class LLMClient {
797
825
  );
798
826
  }
799
827
  }
800
- console.log(
828
+ log.info(
801
829
  `[LLMClient:${label}] Invoking ${this.provider}/${this.modelName} (timeout=${timeoutMs}ms)`,
802
830
  );
803
831
  try {
804
832
  const response = await modelToUse.invoke(langchainMessages, {
805
833
  signal: controller.signal,
806
834
  });
807
- console.log(
835
+ log.info(
808
836
  `[LLMClient:${label}] Responded in ${Date.now() - startedAt}ms`,
809
837
  );
810
838
  return response;
811
839
  } catch (err) {
812
840
  const elapsed = Date.now() - startedAt;
813
841
  const message = err instanceof Error ? err.message : String(err);
814
- console.error(
842
+ log.error(
815
843
  `[LLMClient:${label}] FAILED after ${elapsed}ms: ${message}`,
816
844
  );
817
845
  throw err;
@@ -1026,7 +1054,7 @@ export class LLMClient {
1026
1054
  }
1027
1055
 
1028
1056
  const startedAt = Date.now();
1029
- console.log(
1057
+ log.info(
1030
1058
  `[LLMClient:callWithTools] Invoking ${this.provider}/${this.modelName} (tools=${options.tools.length}, messages=${options.messages.length})`,
1031
1059
  );
1032
1060
 
@@ -1062,7 +1090,7 @@ export class LLMClient {
1062
1090
  if (!response.ok) {
1063
1091
  const errText = await response.text().catch(() => '<no body>');
1064
1092
  const elapsed = Date.now() - startedAt;
1065
- console.error(
1093
+ log.error(
1066
1094
  `[LLMClient:callWithTools] FAILED after ${elapsed}ms (${response.status} ${response.statusText}): ${errText}`,
1067
1095
  );
1068
1096
  throw new Error(
@@ -1112,7 +1140,7 @@ export class LLMClient {
1112
1140
  }
1113
1141
  }
1114
1142
 
1115
- console.log(
1143
+ log.info(
1116
1144
  `[LLMClient:callWithTools] Responded in ${Date.now() - startedAt}ms (prompt=${usage?.promptTokens ?? 0}, completion=${usage?.completionTokens ?? 0}, tool_calls=${choice.message.tool_calls?.length ?? 0})`,
1117
1145
  );
1118
1146
 
@@ -1195,7 +1223,7 @@ export class LLMClient {
1195
1223
  } = options;
1196
1224
 
1197
1225
  if (this.provider !== 'anthropic') {
1198
- console.log(
1226
+ log.info(
1199
1227
  `[LLMClient:callWithCache] Provider ${this.provider} doesn't support caching, using regular call`,
1200
1228
  );
1201
1229
  return this.callWithMetadata(options);
@@ -1204,7 +1232,7 @@ export class LLMClient {
1204
1232
  const cacheableCount =
1205
1233
  (systemBlocks || []).filter((b) => b.cache_control).length +
1206
1234
  (userBlocks || []).filter((b) => b.cache_control).length;
1207
- console.log(
1235
+ log.info(
1208
1236
  `[LLMClient:callWithCache] ${cacheableCount} cacheable block(s)`,
1209
1237
  );
1210
1238
 
@@ -1212,7 +1240,7 @@ export class LLMClient {
1212
1240
 
1213
1241
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
1214
1242
  try {
1215
- console.log(
1243
+ log.info(
1216
1244
  `[LLMClient:callWithCache] Attempt ${attempt + 1}/${maxRetries + 1}...`,
1217
1245
  );
1218
1246
 
@@ -1268,7 +1296,7 @@ export class LLMClient {
1268
1296
  const cacheCreation = apiUsage.cache_creation_input_tokens || 0;
1269
1297
 
1270
1298
  if (cacheCreation > 0) {
1271
- console.log(
1299
+ log.info(
1272
1300
  `[LLMClient:callWithCache] Cache WRITE: ${cacheCreation} tokens`,
1273
1301
  );
1274
1302
  }
@@ -1276,12 +1304,12 @@ export class LLMClient {
1276
1304
  const savingsPercent = Math.round(
1277
1305
  (cacheRead / (cacheRead + apiUsage.input_tokens)) * 100,
1278
1306
  );
1279
- console.log(
1307
+ log.info(
1280
1308
  `[LLMClient:callWithCache] Cache HIT: ${cacheRead} tokens (~${savingsPercent}% of prompt)`,
1281
1309
  );
1282
1310
  }
1283
1311
  if (cacheCreation === 0 && cacheRead === 0) {
1284
- console.log(
1312
+ log.info(
1285
1313
  `[LLMClient:callWithCache] No caching: ${apiUsage.input_tokens} input tokens`,
1286
1314
  );
1287
1315
  }
@@ -1333,9 +1361,8 @@ export class LLMClient {
1333
1361
  };
1334
1362
  } catch (error) {
1335
1363
  lastError = error instanceof Error ? error : new Error(String(error));
1336
- console.error(
1337
- `[LLMClient:callWithCache] Attempt ${attempt + 1} failed:`,
1338
- lastError.message,
1364
+ log.error(
1365
+ `[LLMClient:callWithCache] Attempt ${attempt + 1} failed: ${lastError.message}`,
1339
1366
  );
1340
1367
 
1341
1368
  if (this.isRateLimitError(lastError)) {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/client.ts","../src/tool-call-types.ts"],"sourcesContent":["/**\n * Shared LLM Client\n *\n * Multi-provider LLM client with:\n * - OpenAI, DeepSeek, Anthropic, and Kimi support\n * - Anthropic prompt caching (CachingChatAnthropic)\n * - Rate limiting and retry logic\n * - Token tracking\n * - Structured output parsing with Zod\n *\n * @packageDocumentation\n */\n\nimport { ChatOpenAI } from '@langchain/openai';\nimport { ChatAnthropic } from '@langchain/anthropic';\nimport type { BaseMessageLike } from '@langchain/core/messages';\nimport Anthropic from '@anthropic-ai/sdk';\nimport { z } from 'zod';\nimport {\n RateLimiter,\n getGlobalRateLimiter,\n type RateLimiterOptions,\n} from './rate-limiter.js';\nimport { TokenTracker, getGlobalTokenTracker } from './token-tracker.js';\nimport { parseJsonResponse } from './json-parser.js';\nimport {\n parseChatCompletionResponse,\n type ChatCompletionMessage,\n type ChatCompletionToolDef,\n} from './tool-call-types.js';\n\n// ============================================================================\n// Local type helpers (avoid Record<string, unknown> and unsafe casts)\n// ============================================================================\n\n/** Anthropic generation output with usage metadata (not in Langchain's base types). */\ninterface AnthropicGenerationWithUsage {\n message?: {\n usage_metadata?: {\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n input_tokens?: number;\n output_tokens?: number;\n };\n };\n}\n\n/** Response metadata from OpenAI-compatible providers. */\ninterface OpenAIResponseMetadata {\n finish_reason?: string;\n}\n\n/** Model-specific kwargs passed to ChatOpenAI constructor. */\ninterface ModelKwargs {\n max_completion_tokens?: number;\n thinking?: { type: string };\n tool_choice?: string;\n /** OpenRouter: ask for detailed usage accounting (returns cached-token breakdown). */\n usage?: { include: boolean };\n}\n\n/**\n * Pull the cache-read / cache-write token split out of a LangChain\n * `usage_metadata` object. Every provider LangChain supports normalises\n * its cache-hit tokens into `input_token_details` (OpenAI/OpenRouter/DeepSeek\n * via `cached_tokens`→`cache_read`; Anthropic via `cache_read`/`cache_creation`),\n * so one extractor covers them all. Returns zeros when the provider reports\n * no cache detail — which prices identically to the old flat-rate path.\n */\nfunction cacheTokensFromUsageMetadata(usageMeta: {\n input_tokens?: number;\n output_tokens?: number;\n input_token_details?: { cache_read?: number; cache_creation?: number };\n}): { cachedPromptTokens: number; cacheWriteTokens: number } {\n const details = usageMeta.input_token_details ?? {};\n return {\n cachedPromptTokens: details.cache_read ?? 0,\n cacheWriteTokens: details.cache_creation ?? 0,\n };\n}\n\n/**\n * Identity cast for generic return types.\n * Used when a string value must satisfy a generic T parameter\n * (e.g., rawText mode where caller declares T = string).\n *\n * Safety: callers only reach this path when rawText=true, which\n * constrains T to string by convention. TypeScript cannot verify\n * this constraint statically because T is caller-supplied.\n */\nfunction asGeneric<T>(value: string): T {\n return value as T;\n}\n\n// ============================================================================\n// Anthropic Cache Control Helper\n// ============================================================================\n\nfunction addCacheControlToSystemMessages(\n messages: Array<{ role: string; content: string }>,\n): BaseMessageLike[] {\n return messages.map((msg) => {\n if (msg.role !== 'system') {\n return msg as BaseMessageLike;\n }\n\n return {\n role: msg.role,\n content: [\n {\n type: 'text' as const,\n text: msg.content,\n cache_control: { type: 'ephemeral' },\n },\n ],\n } as BaseMessageLike;\n });\n}\n\ntype ChatModel = ChatOpenAI | ChatAnthropic;\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type LLMProvider = 'openai' | 'deepseek' | 'anthropic' | 'kimi' | 'openrouter' | 'orbgen' | 'masar';\n\nexport interface ProviderConfig {\n apiKey: string;\n baseUrl?: string;\n defaultModel: string;\n}\n\nexport interface LLMClientOptions {\n provider?: LLMProvider;\n model?: string;\n temperature?: number;\n streaming?: boolean;\n rateLimiter?: RateLimiterOptions;\n useGlobalRateLimiter?: boolean;\n trackTokens?: boolean;\n /**\n * Hard per-call deadline (ms) for the raw invoke paths (`callRaw`,\n * `callRawWithMetadata`, `callWithMessages`). Defaults to 5 min. Set this\n * BELOW the caller's own wall-clock cap so a hung connection aborts in\n * time for the caller to recover (e.g. a battery spec capped at 300s sets\n * 120s here so a hang fails fast and leaves budget for a retry, instead of\n * the call consuming the entire 300s).\n */\n rawTimeoutMs?: number;\n /**\n * Explicit provider credentials. When set, used instead of reading the\n * provider's env vars — lets callers (e.g. the CLI's account store) supply\n * `apiKey`/`baseUrl`/`defaultModel` directly. Absent ⇒ env fallback.\n */\n providerConfig?: ProviderConfig;\n}\n\nexport interface LLMCallOptions<T = unknown> {\n systemPrompt: string;\n userPrompt: string;\n schema?: z.ZodSchema<T>;\n maxRetries?: number;\n retryWithContext?: boolean;\n maxTokens?: number;\n skipSchemaValidation?: boolean;\n temperature?: number;\n}\n\nexport interface CacheableBlock {\n type: 'text';\n text: string;\n cache_control?: { type: 'ephemeral' };\n}\n\nexport interface CacheAwareLLMCallOptions<T = unknown>\n extends LLMCallOptions<T> {\n systemBlocks?: CacheableBlock[];\n userBlocks?: CacheableBlock[];\n rawText?: boolean;\n}\n\nexport interface LLMUsage {\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n}\n\nexport type LLMFinishReason =\n | 'stop'\n | 'length'\n | 'content_filter'\n | 'tool_calls'\n | null;\n\nexport interface LLMResponse<T> {\n data: T;\n raw: string;\n finishReason: LLMFinishReason;\n usage: LLMUsage | null;\n}\n\nexport interface LLMStreamOptions {\n systemPrompt: string;\n messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>;\n maxTokens?: number;\n temperature?: number;\n}\n\nexport interface LLMStreamChunk {\n content: string;\n done: boolean;\n}\n\n// ============================================================================\n// Provider Configuration\n// ============================================================================\n\nconst PROVIDER_CONFIGS: Record<LLMProvider, () => ProviderConfig> = {\n openai: () => {\n const apiKey = process.env.OPENAI_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'OPENAI_API_KEY environment variable is not set. ' +\n 'Please set it in your .env file or environment.',\n );\n }\n return { apiKey, baseUrl: undefined, defaultModel: 'gpt-4o' };\n },\n deepseek: () => {\n const apiKey = process.env.DEEPSEEK_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'DEEPSEEK_API_KEY environment variable is not set. ' +\n 'Please set it in your .env file or environment.',\n );\n }\n return {\n apiKey,\n baseUrl: 'https://api.deepseek.com/v1',\n defaultModel: 'deepseek-v4-flash',\n };\n },\n anthropic: () => {\n const apiKey = process.env.ANTHROPIC_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'ANTHROPIC_API_KEY environment variable is not set. ' +\n 'Please set it in your .env file or environment.',\n );\n }\n return {\n apiKey,\n baseUrl: undefined,\n defaultModel: 'claude-sonnet-4-5-20250929',\n };\n },\n kimi: () => {\n const apiKey = process.env.KIMI_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'KIMI_API_KEY environment variable is not set. ' +\n 'Please set it in your .env file or environment.',\n );\n }\n return {\n apiKey,\n baseUrl: 'https://api.moonshot.ai/v1',\n defaultModel: 'kimi-k2.5',\n };\n },\n openrouter: () => {\n const apiKey = process.env.OPEN_ROUTER_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'OPEN_ROUTER_API_KEY environment variable is not set. ' +\n 'Please set it in your .env file or environment.',\n );\n }\n return {\n apiKey,\n baseUrl: 'https://openrouter.ai/api/v1',\n defaultModel: 'qwen/qwen-2.5-72b-instruct', // Default to Qwen 2.5\n };\n },\n orbgen: () => {\n const baseUrl = process.env.ORBGEN_URL;\n if (!baseUrl) {\n throw new Error(\n 'ORBGEN_URL environment variable is not set. ' +\n 'Set it to the OrbGen Cloud Run URL (e.g., https://orbgen-v2-xxx.run.app)',\n );\n }\n return {\n apiKey: 'not-needed',\n baseUrl: `${baseUrl}/v1`,\n defaultModel: 'orbgen-v2',\n };\n },\n masar: () => {\n // Fine-tuned masar subagent served OpenAI-compatibly (vLLM on Cloud Run GPU).\n const baseUrl = process.env.MASAR_SUBAGENT_URL;\n if (!baseUrl) {\n throw new Error(\n 'MASAR_SUBAGENT_URL environment variable is not set. ' +\n 'Set it to the masar subagent endpoint (e.g., https://masar-subagent-xxx.run.app)',\n );\n }\n return {\n apiKey: process.env.MASAR_SUBAGENT_API_KEY ?? 'not-needed',\n baseUrl: `${baseUrl}/v1`,\n defaultModel: process.env.MASAR_SUBAGENT_MODEL ?? 'subagent',\n };\n },\n};\n\nexport const DEEPSEEK_MODELS = {\n CHAT: 'deepseek-chat',\n CODER: 'deepseek-coder',\n REASONER: 'deepseek-reasoner',\n V4_PRO: 'deepseek-v4-pro',\n V4_FLASH: 'deepseek-v4-flash',\n} as const;\n\nexport const OPENAI_MODELS = {\n GPT4O: 'gpt-4o',\n GPT4O_MINI: 'gpt-4o-mini',\n GPT4_TURBO: 'gpt-4-turbo',\n GPT35_TURBO: 'gpt-3.5-turbo',\n GPT_5_1: 'gpt-5.1',\n} as const;\n\nexport const ANTHROPIC_MODELS = {\n CLAUDE_SONNET_4_5: 'claude-sonnet-4-5-20250929',\n CLAUDE_SONNET_4: 'claude-sonnet-4-20250514',\n CLAUDE_OPUS_4_5: 'claude-opus-4-5-20250929',\n CLAUDE_3_5_HAIKU: 'claude-3-5-haiku-20241022',\n} as const;\n\nexport const KIMI_MODELS = {\n K2_5: 'kimi-k2.5',\n} as const;\n\nexport const OPENROUTER_MODELS = {\n // Qwen models - JSON/structured data specialists\n QWEN_2_5_72B: 'qwen/qwen-2.5-72b-instruct',\n QWEN_2_5_CODER_32B: 'qwen/qwen-2.5-coder-32b-instruct',\n QWEN_3_235B: 'qwen/qwen3-235b-a22b',\n\n // Gemma models - best small models for structured JSON output\n // Gemma 3 4B: 6/6 on complex decomposition, 100% behavior matching, free, fastest\n GEMMA_3_4B: 'google/gemma-3-4b-it',\n GEMMA_3_12B: 'google/gemma-3-12b-it',\n GEMMA_3_27B: 'google/gemma-3-27b-it',\n\n // Mistral models - strong structured output, function calling\n MINISTRAL_8B: 'mistralai/ministral-8b-2512',\n // Mistral Small 3.1: 6/6 on complex decomposition, picked std-kanban for tasks\n MISTRAL_SMALL_3_1: 'mistralai/mistral-small-3.1-24b-instruct',\n // Mistral Medium 3.1: next tier up from Small, stronger reasoning, tool calling\n MISTRAL_MEDIUM_3_1: 'mistralai/mistral-medium-3.1',\n\n // Llama models - agentic workhorses\n LLAMA_3_3_70B: 'meta-llama/llama-3.3-70b-instruct',\n LLAMA_3_1_405B: 'meta-llama/llama-3.1-405b-instruct',\n LLAMA_4_MAVERICK: 'meta-llama/llama-4-maverick',\n LLAMA_4_SCOUT: 'meta-llama/llama-4-scout',\n\n // Kimi models - strong reasoning\n KIMI_K2: 'moonshotai/kimi-k2',\n\n // Zhipu GLM models - via OpenRouter\n GLM_4_7: 'z-ai/glm-4.7',\n} as const;\n\nconst DEFAULT_TEMPERATURE = 0.3;\n\n// ============================================================================\n// LLM Client\n// ============================================================================\n\nexport class LLMClient {\n private model: ChatModel;\n private rateLimiter: RateLimiter;\n private tokenTracker: TokenTracker | null;\n private modelName: string;\n private provider: LLMProvider;\n private providerConfig: ProviderConfig;\n private temperature: number;\n private streaming: boolean;\n private rawTimeoutMs: number;\n\n constructor(options: LLMClientOptions = {}) {\n this.provider = options.provider || 'openai';\n // Kimi: 0.6 when thinking disabled (our default), 1.0 when thinking enabled\n this.temperature = options.temperature ?? \n (this.provider === 'kimi' ? 0.6 : DEFAULT_TEMPERATURE);\n this.streaming = options.streaming ?? false;\n\n this.providerConfig = options.providerConfig ?? PROVIDER_CONFIGS[this.provider]();\n this.modelName = options.model || this.providerConfig.defaultModel;\n this.rawTimeoutMs = options.rawTimeoutMs ?? LLMClient.DEFAULT_RAW_TIMEOUT_MS;\n\n const keyPreview = this.providerConfig.apiKey.slice(-4);\n console.log(\n `[LLMClient] Provider: ${this.provider}, Model: ${this.modelName}, Key: ****${keyPreview}`,\n );\n if (this.providerConfig.baseUrl) {\n console.log(\n `[LLMClient] Using custom base URL: ${this.providerConfig.baseUrl}`,\n );\n }\n\n this.model = this.createModel();\n\n this.rateLimiter =\n options.useGlobalRateLimiter !== false\n ? getGlobalRateLimiter(options.rateLimiter)\n : new RateLimiter(options.rateLimiter);\n\n this.tokenTracker =\n options.trackTokens !== false\n ? getGlobalTokenTracker(this.modelName)\n : null;\n }\n\n private usesMaxCompletionTokens(): boolean {\n const model = this.modelName.toLowerCase();\n return (\n model.startsWith('o1') ||\n model.startsWith('gpt-5') ||\n model.includes('o1-') ||\n model.includes('o3')\n );\n }\n\n private createModel(options?: {\n maxTokens?: number;\n temperature?: number;\n }): ChatModel {\n const maxTokens = options?.maxTokens;\n const temperature = options?.temperature ?? this.temperature;\n\n if (this.provider === 'anthropic') {\n return new ChatAnthropic({\n apiKey: this.providerConfig.apiKey,\n model: this.modelName,\n temperature,\n streaming: this.streaming,\n maxTokens: maxTokens || 8192,\n callbacks: [\n {\n handleLLMEnd: (output) => {\n const generation = output.generations?.[0]?.[0];\n const generationWithUsage = generation as\n | (typeof generation & AnthropicGenerationWithUsage)\n | undefined;\n const usage = generationWithUsage?.message?.usage_metadata;\n\n if (usage) {\n const cacheCreated = usage.cache_creation_input_tokens ?? 0;\n const cacheRead = usage.cache_read_input_tokens ?? 0;\n const inputTokens = usage.input_tokens ?? 0;\n const outputTokens = usage.output_tokens ?? 0;\n\n if (cacheCreated > 0) {\n console.log(\n `[LLMClient:Anthropic] Cache WRITE: ${cacheCreated} tokens cached`,\n );\n }\n if (cacheRead > 0) {\n const savingsPercent = Math.round(\n (cacheRead / (cacheRead + inputTokens)) * 100,\n );\n console.log(\n `[LLMClient:Anthropic] Cache HIT: ${cacheRead} tokens (~${savingsPercent}% of prompt)`,\n );\n }\n if (cacheCreated === 0 && cacheRead === 0 && inputTokens > 0) {\n if (inputTokens < 500) {\n console.log(\n `[LLMClient:Anthropic] ${inputTokens} input, ${outputTokens} output tokens (likely cached)`,\n );\n } else {\n console.log(\n `[LLMClient:Anthropic] ${inputTokens} input, ${outputTokens} output tokens`,\n );\n }\n }\n }\n },\n },\n ],\n });\n }\n\n const useCompletionTokens = this.usesMaxCompletionTokens();\n\n const tokenConfig = maxTokens\n ? useCompletionTokens\n ? { modelKwargs: { max_completion_tokens: maxTokens } }\n : { maxTokens }\n : {};\n\n const timeout = this.provider === 'deepseek' ? 600000 : undefined;\n\n // Kimi-k2.5: disable thinking to avoid reasoning_content issues with tool calls\n // When thinking is disabled, temperature must be 0.6 (not 1.0)\n const isKimi = this.provider === 'kimi';\n const effectiveTemp = isKimi ? 0.6 : temperature;\n\n // Build modelKwargs incrementally to avoid spread conflicts\n const modelKwargs: ModelKwargs = {};\n if (useCompletionTokens && maxTokens) {\n modelKwargs.max_completion_tokens = maxTokens;\n }\n if (isKimi) {\n modelKwargs.thinking = { type: 'disabled' };\n }\n // OpenRouter (Qwen): explicit tool_choice so the model doesn't ignore tool definitions\n if (this.provider === 'openrouter') {\n modelKwargs.tool_choice = 'auto';\n // Return the cached-token breakdown so cost is priced cache-aware.\n modelKwargs.usage = { include: true };\n }\n\n return new ChatOpenAI({\n apiKey: this.providerConfig.apiKey,\n model: this.modelName,\n temperature: useCompletionTokens ? undefined : effectiveTemp,\n streaming: this.streaming,\n timeout,\n ...(Object.keys(modelKwargs).length > 0 ? { modelKwargs } : {}),\n ...(useCompletionTokens ? {} : maxTokens ? { maxTokens } : {}),\n configuration: {\n apiKey: this.providerConfig.apiKey,\n ...(this.providerConfig.baseUrl\n ? { baseURL: this.providerConfig.baseUrl }\n : {}),\n },\n });\n }\n\n private getModelWithOptions(options: {\n maxTokens?: number;\n temperature?: number;\n }): ChatModel {\n return this.createModel(options);\n }\n\n /**\n * Check if this model is a Qwen3.5 thinking model.\n * These models burn all output tokens on internal reasoning\n * unless thinking is explicitly disabled via /no_think prefix.\n */\n private isQwenThinkingModel(): boolean {\n return this.modelName.includes('qwen3.5');\n }\n\n /**\n * Prepare user prompt with provider-specific adjustments.\n * Qwen3.5 models require /no_think to disable reasoning mode.\n */\n private prepareUserPrompt(prompt: string): string {\n if (this.isQwenThinkingModel()) {\n return `/no_think\\n${prompt}`;\n }\n return prompt;\n }\n\n getProvider(): LLMProvider {\n return this.provider;\n }\n\n getModelName(): string {\n return this.modelName;\n }\n\n getModel(): ChatModel {\n return this.model;\n }\n\n getRateLimiterStatus() {\n return this.rateLimiter.getStatus();\n }\n\n getTokenUsage() {\n return this.tokenTracker?.getSummary() ?? null;\n }\n\n async call<T>(options: LLMCallOptions<T>): Promise<T> {\n const response = await this.callWithMetadata(options);\n return response.data;\n }\n\n async callWithMetadata<T>(options: LLMCallOptions<T>): Promise<LLMResponse<T>> {\n const {\n systemPrompt,\n userPrompt,\n schema,\n maxRetries = 2,\n retryWithContext = true,\n maxTokens,\n skipSchemaValidation = false,\n temperature,\n } = options;\n\n let currentPrompt = userPrompt;\n let lastError: Error | null = null;\n\n console.log(\n `[LLMClient:call] Starting call to ${this.provider}/${this.modelName}`,\n );\n console.log(`[LLMClient:call] Prompt length: ${userPrompt.length} chars`);\n if (maxTokens) {\n console.log(`[LLMClient:call] Max tokens: ${maxTokens}`);\n }\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n console.log(\n `[LLMClient:call] Attempt ${attempt + 1}/${maxRetries + 1}...`,\n );\n const attemptStartTime = Date.now();\n\n const result = await this.rateLimiter.execute(async () => {\n console.log(`[LLMClient:call] Invoking model...`);\n const invokeStartTime = Date.now();\n\n const modelToUse =\n maxTokens || temperature !== undefined\n ? this.getModelWithOptions({ maxTokens, temperature })\n : this.model;\n\n const messages = [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: this.prepareUserPrompt(currentPrompt) },\n ];\n const response = await modelToUse.invoke(\n this.provider === 'anthropic'\n ? addCacheControlToSystemMessages(messages)\n : messages,\n );\n\n console.log(\n `[LLMClient:call] Model responded in ${Date.now() - invokeStartTime}ms`,\n );\n\n let usage: LLMUsage | null = null;\n if (response.usage_metadata) {\n const usageMeta = response.usage_metadata as {\n input_tokens?: number;\n output_tokens?: number;\n };\n usage = {\n promptTokens: usageMeta.input_tokens || 0,\n completionTokens: usageMeta.output_tokens || 0,\n totalTokens:\n (usageMeta.input_tokens || 0) +\n (usageMeta.output_tokens || 0),\n };\n console.log(\n `[LLMClient:call] Tokens used: ${usage.promptTokens} in, ${usage.completionTokens} out`,\n );\n\n if (this.tokenTracker) {\n this.tokenTracker.addUsage(\n usage.promptTokens,\n usage.completionTokens,\n { provider: this.provider, ...cacheTokensFromUsageMetadata(usageMeta) },\n );\n }\n }\n\n const finishReason = this.extractFinishReason(response);\n if (finishReason === 'length') {\n console.warn(\n `[LLMClient:call] Response truncated (finish_reason=length)`,\n );\n }\n\n const content =\n typeof response.content === 'string'\n ? response.content\n : JSON.stringify(response.content);\n\n console.log(\n `[LLMClient:call] Response length: ${content.length} chars, finish_reason: ${finishReason}`,\n );\n\n return { content, finishReason, usage };\n });\n\n console.log(\n `[LLMClient:call] Attempt ${attempt + 1} completed in ${Date.now() - attemptStartTime}ms, parsing response...`,\n );\n\n const parsed = skipSchemaValidation\n ? (parseJsonResponse(result.content, undefined) as T)\n : parseJsonResponse(result.content, schema);\n console.log(\n `[LLMClient:call] Response parsed successfully${skipSchemaValidation ? ' (schema validation skipped)' : ''}`,\n );\n\n return {\n data: parsed,\n raw: result.content,\n finishReason: result.finishReason,\n usage: result.usage,\n };\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n console.error(\n `[LLMClient:call] Attempt ${attempt + 1} failed:`,\n lastError.message,\n );\n\n if (this.isRateLimitError(lastError)) {\n console.error(`[LLMClient:call] Rate limit error, not retrying`);\n throw lastError;\n }\n\n if (attempt < maxRetries && retryWithContext) {\n console.log(`[LLMClient:call] Will retry with error context`);\n currentPrompt =\n `${userPrompt}\\n\\n` +\n `[Previous attempt failed with: ${lastError.message}]\\n` +\n `Please output valid JSON that matches the expected schema.`;\n }\n }\n }\n\n console.error(`[LLMClient:call] All attempts exhausted, throwing error`);\n throw lastError;\n }\n\n private extractFinishReason(\n response: Awaited<ReturnType<ChatOpenAI['invoke']>>,\n ): LLMFinishReason {\n const metadata = response.response_metadata as\n | OpenAIResponseMetadata\n | undefined;\n if (metadata?.finish_reason) {\n const reason = metadata.finish_reason as string;\n if (\n reason === 'stop' ||\n reason === 'length' ||\n reason === 'content_filter' ||\n reason === 'tool_calls'\n ) {\n return reason;\n }\n }\n return null;\n }\n\n /**\n * Default hard timeout for raw LLM invocations. The langchain ChatOpenAI\n * `timeout` option doesn't reliably fire when an HTTP connection\n * half-opens (the response stream can sit open indefinitely), so every\n * raw call gets wrapped in an AbortController-driven deadline. Callers\n * can override by passing their own `signal`.\n */\n private static readonly DEFAULT_RAW_TIMEOUT_MS = 5 * 60 * 1000; // 5 min\n\n /**\n * Wrap a `model.invoke(...)` with start/finish/error logging and a hard\n * abort-driven timeout. Returns whatever the underlying `invoke` returns.\n *\n * The raw paths (`callRaw`, `callRawWithMetadata`, `callWithMessages`)\n * used to be silent — when a connection half-opened, callers sat\n * indefinitely with no feedback. This wrapper makes hangs visible (start\n * + duration logs) and bounded (timeout fires with a clear error).\n */\n private async invokeWithObservability(\n label: string,\n modelToUse: ChatModel,\n langchainMessages: BaseMessageLike[],\n callerSignal?: AbortSignal,\n timeoutMsArg?: number,\n ): Promise<Awaited<ReturnType<ChatModel['invoke']>>> {\n const timeoutMs = timeoutMsArg ?? this.rawTimeoutMs;\n const startedAt = Date.now();\n const controller = new AbortController();\n const timeoutHandle = setTimeout(\n () => controller.abort(new Error(`LLM call timed out after ${timeoutMs}ms`)),\n timeoutMs,\n );\n if (callerSignal) {\n if (callerSignal.aborted) {\n controller.abort(callerSignal.reason);\n } else {\n callerSignal.addEventListener(\n 'abort',\n () => controller.abort(callerSignal.reason),\n { once: true },\n );\n }\n }\n console.log(\n `[LLMClient:${label}] Invoking ${this.provider}/${this.modelName} (timeout=${timeoutMs}ms)`,\n );\n try {\n const response = await modelToUse.invoke(langchainMessages, {\n signal: controller.signal,\n });\n console.log(\n `[LLMClient:${label}] Responded in ${Date.now() - startedAt}ms`,\n );\n return response;\n } catch (err) {\n const elapsed = Date.now() - startedAt;\n const message = err instanceof Error ? err.message : String(err);\n console.error(\n `[LLMClient:${label}] FAILED after ${elapsed}ms: ${message}`,\n );\n throw err;\n } finally {\n clearTimeout(timeoutHandle);\n }\n }\n\n async callRaw(options: {\n systemPrompt: string;\n userPrompt: string;\n maxTokens?: number;\n signal?: AbortSignal;\n }): Promise<string> {\n const response = await this.callRawWithMetadata(options);\n return response.raw;\n }\n\n async callRawWithMetadata(options: {\n systemPrompt: string;\n userPrompt: string;\n maxTokens?: number;\n signal?: AbortSignal;\n }): Promise<Omit<LLMResponse<string>, 'data'> & { raw: string }> {\n const { systemPrompt, userPrompt, maxTokens, signal } = options;\n\n return this.rateLimiter.execute(async () => {\n const modelToUse = maxTokens\n ? this.getModelWithOptions({ maxTokens })\n : this.model;\n\n const messages = [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: this.prepareUserPrompt(userPrompt) },\n ];\n const response = await this.invokeWithObservability(\n 'callRawWithMetadata',\n modelToUse,\n this.provider === 'anthropic'\n ? addCacheControlToSystemMessages(messages)\n : (messages as BaseMessageLike[]),\n signal,\n );\n\n let usage: LLMUsage | null = null;\n if (response.usage_metadata) {\n const usageMeta = response.usage_metadata as {\n input_tokens?: number;\n output_tokens?: number;\n };\n usage = {\n promptTokens: usageMeta.input_tokens || 0,\n completionTokens: usageMeta.output_tokens || 0,\n totalTokens:\n (usageMeta.input_tokens || 0) + (usageMeta.output_tokens || 0),\n };\n\n if (this.tokenTracker) {\n this.tokenTracker.addUsage(\n usage.promptTokens,\n usage.completionTokens,\n { provider: this.provider, ...cacheTokensFromUsageMetadata(usageMeta) },\n );\n }\n }\n\n const finishReason = this.extractFinishReason(response);\n const content =\n typeof response.content === 'string'\n ? response.content\n : JSON.stringify(response.content);\n\n return { raw: content, finishReason, usage };\n });\n }\n\n /**\n * Call the LLM with a structured messages array.\n *\n * Unlike callRawWithMetadata (which takes systemPrompt + userPrompt strings),\n * this accepts a full conversation history with proper role separation.\n * This enables:\n * - Anthropic prompt caching on message boundaries (not just system prompt)\n * - Proper tool_use/tool_result role handling across providers\n * - Reduced token waste from string concatenation\n *\n * All providers support the messages format:\n * - Anthropic: native messages API with cache_control\n * - DeepSeek: OpenAI-compatible messages via ChatOpenAI\n * - OpenRouter: OpenAI-compatible messages via ChatOpenAI\n */\n async callWithMessages(options: {\n messages: Array<{ role: string; content: string }>;\n maxTokens?: number;\n signal?: AbortSignal;\n }): Promise<Omit<LLMResponse<string>, 'data'> & { raw: string }> {\n const { messages, maxTokens, signal } = options;\n\n return this.rateLimiter.execute(async () => {\n const modelToUse = maxTokens\n ? this.getModelWithOptions({ maxTokens })\n : this.model;\n\n const langchainMessages = this.provider === 'anthropic'\n ? addCacheControlToSystemMessages(messages)\n : (messages as BaseMessageLike[]);\n\n const response = await this.invokeWithObservability(\n 'callWithMessages',\n modelToUse,\n langchainMessages,\n signal,\n );\n\n let usage: LLMUsage | null = null;\n if (response.usage_metadata) {\n const usageMeta = response.usage_metadata as {\n input_tokens?: number;\n output_tokens?: number;\n };\n usage = {\n promptTokens: usageMeta.input_tokens || 0,\n completionTokens: usageMeta.output_tokens || 0,\n totalTokens:\n (usageMeta.input_tokens || 0) + (usageMeta.output_tokens || 0),\n };\n\n if (this.tokenTracker) {\n this.tokenTracker.addUsage(\n usage.promptTokens,\n usage.completionTokens,\n { provider: this.provider, ...cacheTokensFromUsageMetadata(usageMeta) },\n );\n }\n }\n\n const finishReason = this.extractFinishReason(response);\n const content =\n typeof response.content === 'string'\n ? response.content\n : JSON.stringify(response.content);\n\n return { raw: content, finishReason, usage };\n });\n }\n\n /**\n * Tool-calling chat-completion call that speaks the OpenAI wire format\n * directly via `fetch`, bypassing LangChain's `ChatOpenAI` converter.\n *\n * MOTIVATION: LangChain's `convertMessagesToCompletionsMessageParams`\n * silently drops every `additional_kwargs` field except `function_call`\n * and `tool_calls`. DeepSeek V4 thinking-mode requires\n * `reasoning_content` to be echoed back on assistant turns that\n * triggered tool_calls; LangChain's converter strips it, the next\n * round-trip fails with \"400 The reasoning_content in the thinking\n * mode must be passed back to the API.\" This method preserves every\n * assistant field verbatim across round-trips.\n *\n * Supported providers: any OpenAI-compatible endpoint (openai,\n * deepseek, openrouter, kimi, orbgen). Anthropic uses a different\n * wire format and is intentionally not supported here — use\n * `callWithMessages` for Anthropic.\n *\n * Defaults `parallel_tool_calls: false` — sequential tool dispatch is\n * the protocol-safe baseline. Multi-tool-call assistant messages\n * trigger DeepSeek's \"insufficient tool messages\" 400 error.\n */\n async callWithTools(options: {\n messages: ReadonlyArray<ChatCompletionMessage>;\n tools: ReadonlyArray<ChatCompletionToolDef>;\n maxTokens?: number;\n parallelToolCalls?: boolean;\n signal?: AbortSignal;\n /** Per-call deadline (ms). Defaults to `rawTimeoutMs` (5 min unless the\n * client was constructed with a lower value). Bounds the fetch so a\n * hung connection can't run past the caller's own wall-clock cap. */\n timeoutMs?: number;\n }): Promise<{\n message: ChatCompletionMessage;\n finishReason: string;\n usage: LLMUsage | null;\n }> {\n if (this.provider === 'anthropic') {\n throw new Error(\n 'LLMClient.callWithTools: anthropic provider is not supported; use callWithMessages instead',\n );\n }\n return this.rateLimiter.execute(async () => {\n const baseUrl = (this.providerConfig.baseUrl ?? 'https://api.openai.com/v1').replace(/\\/$/, '');\n const url = `${baseUrl}/chat/completions`;\n\n const body: { [key: string]: unknown } = {\n model: this.modelName,\n messages: options.messages,\n parallel_tool_calls: options.parallelToolCalls ?? false,\n temperature: this.temperature,\n };\n if (options.tools.length > 0) body['tools'] = options.tools;\n if (options.maxTokens !== undefined) body['max_tokens'] = options.maxTokens;\n // OpenRouter requires explicit opt-in for `reasoning` (chain-of-thought)\n // on models that support it (Gemma 4, etc.). DeepSeek's native API\n // returns `reasoning_content` without an opt-in; OpenRouter returns\n // the field as `reasoning`. We send the flag always for openrouter —\n // models that don't support it ignore it. The response-side normaliser\n // below maps `reasoning` → `reasoning_content` so downstream\n // consumers see one canonical field name.\n if (this.provider === 'openrouter') {\n body['reasoning'] = { enabled: true };\n // Return authoritative usage accounting (real cost + cached-token split).\n body['usage'] = { include: true };\n }\n\n const startedAt = Date.now();\n console.log(\n `[LLMClient:callWithTools] Invoking ${this.provider}/${this.modelName} (tools=${options.tools.length}, messages=${options.messages.length})`,\n );\n\n // Per-call deadline. callWithTools previously honored only a\n // caller-supplied signal, so when runToolLoop passed none a hung\n // connection ran until the caller's external wall-clock cap (e.g. a\n // 300s battery race), with the subagent producing zero metrics. Drive\n // an AbortController off `timeoutMs ?? rawTimeoutMs` and merge the\n // caller's signal so a hang aborts in-process, fast.\n const effectiveTimeoutMs = options.timeoutMs ?? this.rawTimeoutMs;\n const controller = new AbortController();\n const timeoutHandle = setTimeout(\n () => controller.abort(new Error(`LLMClient.callWithTools timed out after ${effectiveTimeoutMs}ms`)),\n effectiveTimeoutMs,\n );\n const callerSignal = options.signal;\n if (callerSignal) {\n if (callerSignal.aborted) controller.abort(callerSignal.reason);\n else callerSignal.addEventListener('abort', () => controller.abort(callerSignal.reason), { once: true });\n }\n const fetchInit: RequestInit = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.providerConfig.apiKey}`,\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n };\n try {\n const response = await fetch(url, fetchInit);\n\n if (!response.ok) {\n const errText = await response.text().catch(() => '<no body>');\n const elapsed = Date.now() - startedAt;\n console.error(\n `[LLMClient:callWithTools] FAILED after ${elapsed}ms (${response.status} ${response.statusText}): ${errText}`,\n );\n throw new Error(\n `LLMClient.callWithTools: ${this.provider} returned ${response.status} ${response.statusText}: ${errText}`,\n );\n }\n\n const raw = await response.text();\n const parsed = parseChatCompletionResponse(raw);\n const choice = parsed.choices[0];\n if (!choice) {\n throw new Error('LLMClient.callWithTools: no choices in response');\n }\n // Normalise OpenRouter's `reasoning` field to DeepSeek's\n // `reasoning_content` so downstream code (rabit's `fromWireMessage`\n // at runtime/services/tool-loop.ts:103) reads one canonical field\n // for chain-of-thought regardless of provider. Cast through\n // `as { reasoning?: string }` because `ChatCompletionMessage`'s\n // type doesn't declare the off-spec `reasoning` field.\n const msgWithReasoning = choice.message as ChatCompletionMessage & { reasoning?: string };\n if (\n msgWithReasoning.reasoning !== undefined &&\n choice.message.reasoning_content === undefined\n ) {\n choice.message.reasoning_content = msgWithReasoning.reasoning;\n }\n\n let usage: LLMUsage | null = null;\n if (parsed.usage) {\n usage = {\n promptTokens: parsed.usage.prompt_tokens,\n completionTokens: parsed.usage.completion_tokens,\n totalTokens: parsed.usage.total_tokens,\n };\n if (this.tokenTracker) {\n const rawUsage = parsed.usage as {\n prompt_tokens_details?: { cached_tokens?: number };\n cost?: number;\n };\n const cachedTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? 0;\n this.tokenTracker.addUsage(usage.promptTokens, usage.completionTokens, {\n provider: this.provider,\n cachedPromptTokens: cachedTokens,\n // OpenRouter returns the real, routing+cache-adjusted charge here.\n ...(typeof rawUsage.cost === 'number' ? { costUSD: rawUsage.cost } : {}),\n });\n }\n }\n\n console.log(\n `[LLMClient:callWithTools] Responded in ${Date.now() - startedAt}ms (prompt=${usage?.promptTokens ?? 0}, completion=${usage?.completionTokens ?? 0}, tool_calls=${choice.message.tool_calls?.length ?? 0})`,\n );\n\n return {\n message: choice.message,\n finishReason: choice.finish_reason,\n usage,\n };\n } finally {\n clearTimeout(timeoutHandle);\n }\n });\n }\n\n /**\n * Stream a raw text response as an async iterator of content chunks.\n * Uses the underlying LangChain model's .stream() method.\n *\n * @param options - System prompt plus full message history\n * @yields LLMStreamChunk with content deltas and a done flag\n */\n async *streamRaw(options: LLMStreamOptions): AsyncGenerator<LLMStreamChunk> {\n const { messages, maxTokens, temperature } = options;\n\n const modelToUse = (maxTokens || temperature !== undefined)\n ? this.getModelWithOptions({ maxTokens, temperature })\n : this.model;\n\n const langchainMessages = this.provider === 'anthropic'\n ? addCacheControlToSystemMessages(messages)\n : messages;\n\n const stream = await modelToUse.stream(langchainMessages);\n\n for await (const chunk of stream) {\n const content = typeof chunk.content === 'string'\n ? chunk.content\n : Array.isArray(chunk.content)\n ? chunk.content\n .filter((c): c is { type: 'text'; text: string } => typeof c === 'object' && c !== null && 'text' in c)\n .map((c) => c.text)\n .join('')\n : '';\n\n if (content) {\n yield { content, done: false };\n }\n }\n\n yield { content: '', done: true };\n }\n\n private isRateLimitError(error: Error): boolean {\n const message = error.message.toLowerCase();\n return (\n message.includes('rate limit') ||\n message.includes('429') ||\n message.includes('quota exceeded')\n );\n }\n\n // ==========================================================================\n // Anthropic Cache Control Support\n // ==========================================================================\n\n async callWithCache<T>(\n options: CacheAwareLLMCallOptions<T>,\n ): Promise<LLMResponse<T>> {\n const {\n systemPrompt,\n userPrompt,\n systemBlocks,\n userBlocks,\n schema,\n maxRetries = 2,\n maxTokens,\n skipSchemaValidation = false,\n temperature,\n rawText = false,\n } = options;\n\n if (this.provider !== 'anthropic') {\n console.log(\n `[LLMClient:callWithCache] Provider ${this.provider} doesn't support caching, using regular call`,\n );\n return this.callWithMetadata(options);\n }\n\n const cacheableCount =\n (systemBlocks || []).filter((b) => b.cache_control).length +\n (userBlocks || []).filter((b) => b.cache_control).length;\n console.log(\n `[LLMClient:callWithCache] ${cacheableCount} cacheable block(s)`,\n );\n\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n console.log(\n `[LLMClient:callWithCache] Attempt ${attempt + 1}/${maxRetries + 1}...`,\n );\n\n const result = await this.rateLimiter.execute(async () => {\n const anthropic = new Anthropic();\n\n const systemContent =\n systemBlocks && systemBlocks.length > 0\n ? systemBlocks.map((b) => ({\n type: 'text' as const,\n text: b.text,\n ...(b.cache_control\n ? { cache_control: b.cache_control }\n : {}),\n }))\n : systemPrompt\n ? [{ type: 'text' as const, text: systemPrompt }]\n : [];\n\n const userContent =\n userBlocks && userBlocks.length > 0\n ? userBlocks.map((b) => ({\n type: 'text' as const,\n text: b.text,\n ...(b.cache_control\n ? { cache_control: b.cache_control }\n : {}),\n }))\n : userPrompt\n ? [{ type: 'text' as const, text: userPrompt }]\n : [];\n\n const response = await anthropic.messages.create({\n model: this.modelName,\n max_tokens: maxTokens || 8192,\n temperature: temperature ?? 0,\n system: systemContent,\n messages: [{ role: 'user', content: userContent }],\n });\n\n const textContent = response.content.find((c) => c.type === 'text');\n const content =\n textContent && 'text' in textContent ? textContent.text : '';\n\n const apiUsage = response.usage as {\n input_tokens: number;\n output_tokens: number;\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n };\n\n const cacheRead = apiUsage.cache_read_input_tokens || 0;\n const cacheCreation = apiUsage.cache_creation_input_tokens || 0;\n\n if (cacheCreation > 0) {\n console.log(\n `[LLMClient:callWithCache] Cache WRITE: ${cacheCreation} tokens`,\n );\n }\n if (cacheRead > 0) {\n const savingsPercent = Math.round(\n (cacheRead / (cacheRead + apiUsage.input_tokens)) * 100,\n );\n console.log(\n `[LLMClient:callWithCache] Cache HIT: ${cacheRead} tokens (~${savingsPercent}% of prompt)`,\n );\n }\n if (cacheCreation === 0 && cacheRead === 0) {\n console.log(\n `[LLMClient:callWithCache] No caching: ${apiUsage.input_tokens} input tokens`,\n );\n }\n\n const usage: LLMUsage = {\n promptTokens: apiUsage.input_tokens,\n completionTokens: apiUsage.output_tokens,\n totalTokens: apiUsage.input_tokens + apiUsage.output_tokens,\n };\n\n if (this.tokenTracker) {\n // Anthropic reports input_tokens as the UNCACHED count; cache reads\n // and writes are separate. Pass the true total so the tracker prices\n // each bucket at its own rate.\n this.tokenTracker.addUsage(\n apiUsage.input_tokens + cacheRead + cacheCreation,\n usage.completionTokens,\n { provider: this.provider, cachedPromptTokens: cacheRead, cacheWriteTokens: cacheCreation },\n );\n }\n\n const finishReason =\n response.stop_reason === 'end_turn'\n ? 'stop'\n : response.stop_reason;\n\n return {\n content,\n finishReason: finishReason as LLMFinishReason,\n usage,\n };\n });\n\n let parsed: T;\n if (rawText) {\n // rawText mode: caller expects T = string; content is already a string\n parsed = asGeneric<T>(result.content);\n } else if (skipSchemaValidation) {\n parsed = parseJsonResponse(result.content, undefined) as T;\n } else {\n parsed = parseJsonResponse(result.content, schema);\n }\n\n return {\n data: parsed,\n raw: result.content,\n finishReason: result.finishReason,\n usage: result.usage,\n };\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n console.error(\n `[LLMClient:callWithCache] Attempt ${attempt + 1} failed:`,\n lastError.message,\n );\n\n if (this.isRateLimitError(lastError)) {\n throw lastError;\n }\n }\n }\n\n throw lastError;\n }\n\n static cacheableBlock(text: string, cache = true): CacheableBlock {\n return cache\n ? { type: 'text', text, cache_control: { type: 'ephemeral' } }\n : { type: 'text', text };\n }\n}\n\n// ============================================================================\n// Singleton Instances\n// ============================================================================\n\nconst sharedClients: Partial<Record<LLMProvider, LLMClient>> = {};\n\nexport function getSharedLLMClient(options?: LLMClientOptions): LLMClient {\n const provider = options?.provider || 'openai';\n if (!sharedClients[provider]) {\n sharedClients[provider] = new LLMClient(options);\n }\n return sharedClients[provider]!;\n}\n\nexport function resetSharedLLMClient(provider?: LLMProvider): void {\n if (provider) {\n delete sharedClients[provider];\n } else {\n for (const key of Object.keys(sharedClients) as LLMProvider[]) {\n delete sharedClients[key];\n }\n }\n}\n\n// ============================================================================\n// Provider Detection\n// ============================================================================\n\nexport function getAvailableProvider(): LLMProvider {\n if (process.env.ANTHROPIC_API_KEY) return 'anthropic';\n if (process.env.DEEPSEEK_API_KEY) return 'deepseek';\n if (process.env.KIMI_API_KEY) return 'kimi';\n if (process.env.OPENAI_API_KEY) return 'openai';\n throw new Error(\n 'No LLM API key found. Please set ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, or KIMI_API_KEY.',\n );\n}\n\nexport function isProviderAvailable(provider: LLMProvider): boolean {\n switch (provider) {\n case 'openai':\n return !!process.env.OPENAI_API_KEY;\n case 'deepseek':\n return !!process.env.DEEPSEEK_API_KEY;\n case 'anthropic':\n return !!process.env.ANTHROPIC_API_KEY;\n case 'kimi':\n return !!process.env.KIMI_API_KEY;\n case 'openrouter':\n return !!process.env.OPEN_ROUTER_API_KEY;\n case 'orbgen':\n return !!process.env.ORBGEN_URL;\n case 'masar':\n return !!process.env.MASAR_SUBAGENT_URL;\n default:\n return false;\n }\n}\n\n// ============================================================================\n// Convenience Functions\n// ============================================================================\n\n/**\n * Create an LLM client optimized for requirements analysis.\n *\n * Uses lower temperature (0.3) for more deterministic output.\n * Defaults to GPT-5.1 for OpenAI or DeepSeek Chat.\n *\n * @param {Partial<LLMClientOptions>} [options] - Optional client configuration\n * @returns {LLMClient} Configured LLM client\n */\nexport function createRequirementsClient(\n options?: Partial<LLMClientOptions>,\n): LLMClient {\n const provider = options?.provider || getAvailableProvider();\n const defaultModel =\n provider === 'deepseek' ? DEEPSEEK_MODELS.CHAT : OPENAI_MODELS.GPT_5_1;\n return new LLMClient({\n provider,\n model: defaultModel,\n temperature: 0.3,\n ...options,\n });\n}\n\n/**\n * Create an LLM client optimized for creative tasks.\n *\n * Uses higher temperature (0.7) for more varied output.\n * Defaults to GPT-4o or DeepSeek Reasoner.\n *\n * @param {Partial<LLMClientOptions>} [options] - Optional client configuration\n * @returns {LLMClient} Configured LLM client\n */\nexport function createCreativeClient(\n options?: Partial<LLMClientOptions>,\n): LLMClient {\n const provider = options?.provider || getAvailableProvider();\n const defaultModel =\n provider === 'deepseek' ? DEEPSEEK_MODELS.REASONER : OPENAI_MODELS.GPT4O;\n return new LLMClient({\n provider,\n model: defaultModel,\n temperature: 0.7,\n ...options,\n });\n}\n\n/**\n * Create an LLM client optimized for code fixing.\n *\n * Uses low temperature (0.2) for precise, deterministic fixes.\n * Defaults to GPT-4o Mini or DeepSeek Chat for cost efficiency.\n *\n * @param {Partial<LLMClientOptions>} [options] - Optional client configuration\n * @returns {LLMClient} Configured LLM client\n */\nexport function createFixClient(\n options?: Partial<LLMClientOptions>,\n): LLMClient {\n const provider = options?.provider || getAvailableProvider();\n const defaultModel =\n provider === 'deepseek'\n ? DEEPSEEK_MODELS.CHAT\n : OPENAI_MODELS.GPT4O_MINI;\n return new LLMClient({\n provider,\n model: defaultModel,\n temperature: 0.2,\n ...options,\n });\n}\n\n/**\n * Create a DeepSeek LLM client.\n *\n * @param {Partial<Omit<LLMClientOptions, 'provider'>>} [options] - Optional client configuration\n * @returns {LLMClient} Configured DeepSeek client\n */\nexport function createDeepSeekClient(\n options?: Partial<Omit<LLMClientOptions, 'provider'>>,\n): LLMClient {\n return new LLMClient({\n provider: 'deepseek',\n model: DEEPSEEK_MODELS.CHAT,\n ...options,\n });\n}\n\n/**\n * Create an OpenAI LLM client.\n *\n * @param {Partial<Omit<LLMClientOptions, 'provider'>>} [options] - Optional client configuration\n * @returns {LLMClient} Configured OpenAI client\n */\nexport function createOpenAIClient(\n options?: Partial<Omit<LLMClientOptions, 'provider'>>,\n): LLMClient {\n return new LLMClient({\n provider: 'openai',\n model: OPENAI_MODELS.GPT4O,\n ...options,\n });\n}\n\n/**\n * Create an Anthropic LLM client.\n *\n * @param {Partial<Omit<LLMClientOptions, 'provider'>>} [options] - Optional client configuration\n * @returns {LLMClient} Configured Anthropic client\n */\nexport function createAnthropicClient(\n options?: Partial<Omit<LLMClientOptions, 'provider'>>,\n): LLMClient {\n return new LLMClient({\n provider: 'anthropic',\n model: ANTHROPIC_MODELS.CLAUDE_SONNET_4_5,\n ...options,\n });\n}\n\n/**\n * Create a Kimi LLM client.\n *\n * @param {Partial<Omit<LLMClientOptions, 'provider'>>} [options] - Optional client configuration\n * @returns {LLMClient} Configured Kimi client\n */\nexport function createKimiClient(\n options?: Partial<Omit<LLMClientOptions, 'provider'>>,\n): LLMClient {\n return new LLMClient({\n provider: 'kimi',\n model: KIMI_MODELS.K2_5,\n ...options,\n });\n}\n\n/**\n * Create an OpenRouter LLM client.\n *\n * @param {Partial<Omit<LLMClientOptions, 'provider'>>} [options] - Optional client configuration\n * @returns {LLMClient} Configured OpenRouter client\n */\nexport function createOpenRouterClient(\n options?: Partial<Omit<LLMClientOptions, 'provider'>>,\n): LLMClient {\n return new LLMClient({\n provider: 'openrouter',\n model: OPENROUTER_MODELS.QWEN_2_5_72B,\n ...options,\n });\n}\n\n/**\n * Create a Zhipu (GLM) LLM client via OpenRouter.\n *\n * @param {Partial<Omit<LLMClientOptions, 'provider'>>} [options] - Optional client configuration\n * @returns {LLMClient} Configured Zhipu client\n */\nexport function createZhipuClient(\n options?: Partial<Omit<LLMClientOptions, 'provider'>>,\n): LLMClient {\n return new LLMClient({\n provider: 'openrouter',\n model: OPENROUTER_MODELS.GLM_4_7,\n ...options,\n });\n}\n","/**\n * OpenAI Chat Completions wire-format types used by `LLMClient.callWithTools`.\n *\n * These mirror the public OpenAI Chat Completions API spec, which is also\n * the protocol every OpenAI-compatible provider (DeepSeek, OpenRouter,\n * Kimi, OrbGen, etc.) implements. The types are intentionally faithful\n * to the wire format — when the LLM emits a `reasoning_content` field\n * (DeepSeek V4 thinking mode), it's preserved verbatim and echoed back\n * on the next round-trip.\n */\n\nimport type { JsonSchema } from '@almadar/core';\n\n// ============================================================================\n// Tool definitions sent to the API\n// ============================================================================\n\nexport interface ChatCompletionToolDef {\n type: 'function';\n function: {\n name: string;\n description: string;\n /** JSON Schema describing the tool's parameters. Sent verbatim to the\n * provider as the tool-call `function.parameters` field. */\n parameters: JsonSchema;\n };\n}\n\n// ============================================================================\n// Message shape (used in both request and response)\n// ============================================================================\n\nexport type ChatCompletionRole = 'system' | 'user' | 'assistant' | 'tool';\n\nexport interface ChatCompletionToolCall {\n id: string;\n type: 'function';\n function: { name: string; arguments: string };\n}\n\nexport interface ChatCompletionMessage {\n role: ChatCompletionRole;\n /** Null is valid (assistant-only) when the message exists purely to carry `tool_calls`. */\n content: string | null;\n /** Present on assistant turns that called one or more tools. */\n tool_calls?: ChatCompletionToolCall[];\n /** Present on tool-role messages — matches `tool_calls[*].id` of the preceding assistant turn. */\n tool_call_id?: string;\n /**\n * DeepSeek V4 thinking-mode chain-of-thought string. Must be echoed\n * back on the next round-trip when the assistant turn triggered\n * tool_calls — that's the protocol contract that LangChain's\n * ChatOpenAI converter breaks.\n */\n reasoning_content?: string;\n}\n\n// ============================================================================\n// Response shape\n// ============================================================================\n\nexport interface ChatCompletionChoice {\n index: number;\n message: ChatCompletionMessage;\n finish_reason: string;\n}\n\nexport interface ChatCompletionUsage {\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n}\n\nexport interface ChatCompletionResponse {\n choices: ChatCompletionChoice[];\n usage?: ChatCompletionUsage;\n}\n\n// ============================================================================\n// Response parsing — strict type narrowing without `as unknown as`\n// ============================================================================\n\nexport function parseChatCompletionResponse(raw: string): ChatCompletionResponse {\n let json: unknown;\n try {\n json = JSON.parse(raw);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n throw new Error(\n `parseChatCompletionResponse: not valid JSON (${reason}): ${raw.slice(0, 400)}`,\n );\n }\n if (!isChatCompletionResponse(json)) {\n throw new Error(\n `parseChatCompletionResponse: response did not match expected shape: ${raw.slice(0, 400)}`,\n );\n }\n return json;\n}\n\nfunction isChatCompletionResponse(value: unknown): value is ChatCompletionResponse {\n if (value === null || typeof value !== 'object') return false;\n const obj = value as { choices?: unknown };\n if (!Array.isArray(obj.choices)) return false;\n for (const c of obj.choices) {\n if (!isChatCompletionChoice(c)) return false;\n }\n return true;\n}\n\nfunction isChatCompletionChoice(value: unknown): value is ChatCompletionChoice {\n if (value === null || typeof value !== 'object') return false;\n const c = value as { message?: unknown; finish_reason?: unknown; index?: unknown };\n if (typeof c.finish_reason !== 'string') return false;\n if (typeof c.index !== 'number') return false;\n if (!isChatCompletionMessage(c.message)) return false;\n return true;\n}\n\nfunction isChatCompletionMessage(value: unknown): value is ChatCompletionMessage {\n if (value === null || typeof value !== 'object') return false;\n const m = value as { role?: unknown; content?: unknown };\n if (typeof m.role !== 'string') return false;\n if (m.content !== null && typeof m.content !== 'string') return false;\n return true;\n}\n"],"mappings":"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","names":[]}