@almadar/llm 2.41.0 → 2.43.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.
@@ -186,6 +186,14 @@ var PROVIDER_CONFIGS = {
186
186
  baseUrl: `${baseUrl}/v1`,
187
187
  defaultModel: process.env.MASAR_SUBAGENT_MODEL ?? "subagent"
188
188
  };
189
+ },
190
+ lmstudio: () => {
191
+ const baseUrl = process.env.LMSTUDIO_URL ?? "http://127.0.0.1:1234/v1";
192
+ return {
193
+ apiKey: "not-needed",
194
+ baseUrl,
195
+ defaultModel: process.env.LMSTUDIO_MODEL ?? "qwen3-30b-a3b"
196
+ };
189
197
  }
190
198
  };
191
199
  var DEEPSEEK_MODELS = {
@@ -240,6 +248,19 @@ var OPENROUTER_MODELS = {
240
248
  // Zhipu GLM models - via OpenRouter
241
249
  GLM_4_7: "z-ai/glm-4.7"
242
250
  };
251
+ var PROVIDER_CATALOG = [
252
+ { provider: "anthropic", models: Object.values(ANTHROPIC_MODELS), defaultModel: ANTHROPIC_MODELS.CLAUDE_SONNET_4_5 },
253
+ { provider: "openai", models: Object.values(OPENAI_MODELS), defaultModel: OPENAI_MODELS.GPT_5_1 },
254
+ { provider: "deepseek", models: Object.values(DEEPSEEK_MODELS), defaultModel: DEEPSEEK_MODELS.CHAT },
255
+ { provider: "kimi", models: Object.values(KIMI_MODELS), defaultModel: KIMI_MODELS.K2_5 },
256
+ { provider: "openrouter", models: Object.values(OPENROUTER_MODELS), defaultModel: OPENROUTER_MODELS.QWEN_3_235B },
257
+ { provider: "orbgen", models: [], defaultModel: "orbgen-v2" },
258
+ { provider: "masar", models: [], defaultModel: "subagent" },
259
+ { provider: "lmstudio", models: [], defaultModel: "qwen3-30b-a3b" }
260
+ ];
261
+ function listSelectableProviders() {
262
+ return PROVIDER_CATALOG.filter((p) => p.models.length > 0);
263
+ }
243
264
  var DEFAULT_TEMPERATURE = 0.3;
244
265
  var _LLMClient = class _LLMClient {
245
266
  constructor(options = {}) {
@@ -252,7 +273,7 @@ var _LLMClient = class _LLMClient {
252
273
  this.providerConfig = { ...this.providerConfig, baseUrl: defaultBaseUrl };
253
274
  }
254
275
  this.modelName = options.model || this.providerConfig.defaultModel;
255
- this.rawTimeoutMs = options.rawTimeoutMs ?? _LLMClient.DEFAULT_RAW_TIMEOUT_MS;
276
+ this.rawTimeoutMs = options.rawTimeoutMs ?? (this.provider === "lmstudio" ? _LLMClient.LOCAL_PROVIDER_RAW_TIMEOUT_MS : _LLMClient.DEFAULT_RAW_TIMEOUT_MS);
256
277
  this.reasoningEffort = options.reasoningEffort;
257
278
  const keyPreview = this.providerConfig.apiKey.slice(-4);
258
279
  log.info(
@@ -515,7 +536,7 @@ Please output valid JSON that matches the expected schema.`;
515
536
  }
516
537
  return null;
517
538
  }
518
- // 5 min
539
+ // 20 min
519
540
  /**
520
541
  * Wrap a `model.invoke(...)` with start/finish/error logging and a hard
521
542
  * abort-driven timeout. Returns whatever the underlying `invoke` returns.
@@ -994,6 +1015,17 @@ Please output valid JSON that matches the expected schema.`;
994
1015
  * can override by passing their own `signal`.
995
1016
  */
996
1017
  _LLMClient.DEFAULT_RAW_TIMEOUT_MS = 5 * 60 * 1e3;
1018
+ // 5 min
1019
+ /**
1020
+ * Local/self-hosted providers (e.g. a quantized model on consumer
1021
+ * hardware) routinely need far longer than 5 min for a single
1022
+ * tool-calling turn under heavy reasoning + a large tool schema — the
1023
+ * 5 min default was observed clipping every attempt at exactly
1024
+ * 300000ms with no completion in sight, not failing on genuinely slow
1025
+ * output. Give them more room; still finite so a truly wedged call
1026
+ * doesn't hang forever.
1027
+ */
1028
+ _LLMClient.LOCAL_PROVIDER_RAW_TIMEOUT_MS = 20 * 60 * 1e3;
997
1029
  var LLMClient = _LLMClient;
998
1030
  var sharedClients = {};
999
1031
  function getSharedLLMClient(options) {
@@ -1037,6 +1069,9 @@ function isProviderAvailable(provider) {
1037
1069
  return !!process.env.ORBGEN_URL;
1038
1070
  case "masar":
1039
1071
  return !!process.env.MASAR_SUBAGENT_URL;
1072
+ case "lmstudio":
1073
+ return true;
1074
+ // local background service with a sane default URL, no env var required
1040
1075
  default:
1041
1076
  return false;
1042
1077
  }
@@ -1122,6 +1157,8 @@ export {
1122
1157
  ANTHROPIC_MODELS,
1123
1158
  KIMI_MODELS,
1124
1159
  OPENROUTER_MODELS,
1160
+ PROVIDER_CATALOG,
1161
+ listSelectableProviders,
1125
1162
  LLMClient,
1126
1163
  getSharedLLMClient,
1127
1164
  resetSharedLLMClient,
@@ -1137,4 +1174,4 @@ export {
1137
1174
  createOpenRouterClient,
1138
1175
  createZhipuClient
1139
1176
  };
1140
- //# sourceMappingURL=chunk-EOZGJJ4J.js.map
1177
+ //# sourceMappingURL=chunk-544CU3RT.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 /** DeepSeek V4: controls reasoning effort (\"none\" disables thinking tokens). */\n reasoning_effort?: string;\n /** OpenRouter: `{ enabled: false }` stops the router burning tokens on CoT. */\n reasoning?: { enabled: 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' | 'lmstudio';\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 * Reasoning effort for thinking models (e.g. DeepSeek V4).\n * Set to \"none\" to disable reasoning tokens for faster content generation.\n */\n reasoningEffort?: string;\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 /** Prompt tokens served from the provider's prefix cache (OpenAI-style\n * `prompt_tokens_details.cached_tokens`, deepseek-native\n * `prompt_cache_hit_tokens`). Absent when the provider reports neither. */\n cachedPromptTokens?: 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// Vision (Anthropic image content-parts)\n// ============================================================================\n\n/** Media types the Anthropic base64 image source accepts. */\nexport type VisionImageMediaType = Anthropic.Base64ImageSource['media_type'];\n\n/** A single image sent as a base64 content-part (no `data:` URI prefix). */\nexport interface VisionImagePart {\n base64: string;\n mediaType: VisionImageMediaType;\n}\n\nexport interface VisionCallOptions<T = string> {\n systemPrompt?: string;\n userText: string;\n images: ReadonlyArray<VisionImagePart>;\n /** Zod schema for structured JSON output — reuses the package's `parseJsonResponse` mechanism. */\n schema?: z.ZodSchema<T>;\n maxTokens?: number;\n temperature?: number;\n skipSchemaValidation?: boolean;\n signal?: AbortSignal;\n}\n\n/**\n * Build the Anthropic user-message content array for a vision call: one text\n * block followed by one image block per screenshot, grounded entirely in the\n * SDK's own typed shapes. Pure and side-effect-free so it can be unit-tested\n * without any API call.\n */\nexport function buildVisionMessageContent(\n userText: string,\n images: ReadonlyArray<VisionImagePart>,\n): Anthropic.ContentBlockParam[] {\n const textBlock: Anthropic.TextBlockParam = { type: 'text', text: userText };\n const imageBlocks: Anthropic.ImageBlockParam[] = images.map((img) => ({\n type: 'image',\n source: { type: 'base64', media_type: img.mediaType, data: img.base64 },\n }));\n return [textBlock, ...imageBlocks];\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 lmstudio: () => {\n // Local LM Studio server (OpenAI-compatible), running as a background\n // service — no env var required, unlike orbgen/masar's remote endpoints.\n const baseUrl = process.env.LMSTUDIO_URL ?? 'http://127.0.0.1:1234/v1';\n return {\n apiKey: 'not-needed',\n baseUrl,\n defaultModel: process.env.LMSTUDIO_MODEL ?? 'qwen3-30b-a3b',\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_HAIKU_4_5: 'claude-haiku-4-5',\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 // Gemma 4 26B A4B (MoE, 4B active) — matrix candidate vs deepseek default (G-MODEL-1)\n GEMMA_4_26B_A4B: 'google/gemma-4-26b-a4b-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\nexport interface ProviderCatalogEntry {\n provider: LLMProvider;\n /** Enumerable models for this provider; empty for env/URL-driven\n * providers (orbgen, masar, lmstudio) with no closed set to choose from. */\n models: string[];\n defaultModel: string;\n}\n\n/** Single source of truth for \"what providers/models does this package\n * support\" — consumers (setup wizards, model pickers) should read this\n * instead of importing individual `*_MODELS` consts and re-deriving the\n * provider list themselves. */\nexport const PROVIDER_CATALOG: ProviderCatalogEntry[] = [\n { provider: 'anthropic', models: Object.values(ANTHROPIC_MODELS), defaultModel: ANTHROPIC_MODELS.CLAUDE_SONNET_4_5 },\n { provider: 'openai', models: Object.values(OPENAI_MODELS), defaultModel: OPENAI_MODELS.GPT_5_1 },\n { provider: 'deepseek', models: Object.values(DEEPSEEK_MODELS), defaultModel: DEEPSEEK_MODELS.CHAT },\n { provider: 'kimi', models: Object.values(KIMI_MODELS), defaultModel: KIMI_MODELS.K2_5 },\n { provider: 'openrouter', models: Object.values(OPENROUTER_MODELS), defaultModel: OPENROUTER_MODELS.QWEN_3_235B },\n { provider: 'orbgen', models: [], defaultModel: 'orbgen-v2' },\n { provider: 'masar', models: [], defaultModel: 'subagent' },\n { provider: 'lmstudio', models: [], defaultModel: 'qwen3-30b-a3b' },\n];\n\n/** Providers with an enumerable model catalog — safe to offer in a picker. */\nexport function listSelectableProviders(): ProviderCatalogEntry[] {\n return PROVIDER_CATALOG.filter((p) => p.models.length > 0);\n}\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 private reasoningEffort: string | undefined;\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 ??\n (this.provider === 'lmstudio' ? LLMClient.LOCAL_PROVIDER_RAW_TIMEOUT_MS : LLMClient.DEFAULT_RAW_TIMEOUT_MS);\n this.reasoningEffort = options.reasoningEffort;\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 streaming?: boolean;\n }): ChatModel {\n const maxTokens = options?.maxTokens;\n const temperature = options?.temperature ?? this.temperature;\n const streaming = options?.streaming ?? this.streaming;\n\n if (this.provider === 'anthropic') {\n return new ChatAnthropic({\n apiKey: this.providerConfig.apiKey,\n model: this.modelName,\n temperature,\n 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 // DeepSeek V4: disable reasoning when reasoningEffort is set\n if (this.reasoningEffort) {\n modelKwargs.reasoning_effort = this.reasoningEffort;\n }\n // OpenRouter: explicit tool_choice so the model doesn't ignore tool definitions.\n // When reasoning is disabled (reasoningEffort === 'none'), send\n // reasoning: { enabled: false } so OpenRouter doesn't burn tokens on CoT.\n if (this.provider === 'openrouter') {\n modelKwargs.tool_choice = 'auto';\n modelKwargs.usage = { include: true };\n if (this.reasoningEffort === 'none') {\n modelKwargs.reasoning = { enabled: false };\n }\n }\n\n return new ChatOpenAI({\n apiKey: this.providerConfig.apiKey,\n model: this.modelName,\n temperature: useCompletionTokens ? undefined : effectiveTemp,\n 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 streaming?: boolean;\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 * Local/self-hosted providers (e.g. a quantized model on consumer\n * hardware) routinely need far longer than 5 min for a single\n * tool-calling turn under heavy reasoning + a large tool schema — the\n * 5 min default was observed clipping every attempt at exactly\n * 300000ms with no completion in sight, not failing on genuinely slow\n * output. Give them more room; still finite so a truly wedged call\n * doesn't hang forever.\n */\n private static readonly LOCAL_PROVIDER_RAW_TIMEOUT_MS = 20 * 60 * 1000; // 20 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 // This raw-fetch path bypasses createModel()'s modelKwargs, so the\n // reasoning kill-switch must be applied here as well — without it\n // DeepSeek V4 burns the whole completion cap on reasoning_content\n // before emitting a tool call (rabit's runaway-reasoning class).\n if (this.reasoningEffort) {\n body['reasoning_effort'] = this.reasoningEffort;\n }\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'] = this.reasoningEffort === 'none' ? { enabled: false } : { 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 const rawUsage = parsed.usage as {\n prompt_tokens_details?: { cached_tokens?: number };\n prompt_cache_hit_tokens?: number;\n cost?: number;\n };\n const cachedTokens =\n rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens;\n usage = {\n promptTokens: parsed.usage.prompt_tokens,\n completionTokens: parsed.usage.completion_tokens,\n totalTokens: parsed.usage.total_tokens,\n ...(cachedTokens !== undefined ? { cachedPromptTokens: cachedTokens } : {}),\n };\n if (this.tokenTracker) {\n this.tokenTracker.addUsage(usage.promptTokens, usage.completionTokens, {\n provider: this.provider,\n cachedPromptTokens: cachedTokens ?? 0,\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 // streamRaw must always stream: force streaming on the model regardless of the client's\n // default `streaming` flag, or LangChain's .stream() degrades to a single whole-response chunk.\n const modelToUse = this.getModelWithOptions({ maxTokens, temperature, streaming: true });\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 /**\n * Send one or more images (base64 content-parts) plus a text prompt to a\n * vision-capable Anthropic model and return the response. When a `schema`\n * is supplied the text output is parsed and validated through the package's\n * `parseJsonResponse` mechanism; otherwise the raw text is returned as `T`.\n *\n * Anthropic-only — image content-parts use the native messages API. Other\n * providers throw (mirrors `callWithTools`).\n */\n async callWithVision<T = string>(\n options: VisionCallOptions<T>,\n ): Promise<LLMResponse<T>> {\n if (this.provider !== 'anthropic') {\n throw new Error(\n `LLMClient.callWithVision: vision is only supported on the anthropic provider (got ${this.provider})`,\n );\n }\n\n const {\n systemPrompt,\n userText,\n images,\n schema,\n maxTokens,\n temperature,\n skipSchemaValidation = false,\n signal,\n } = options;\n\n if (images.length === 0) {\n throw new Error('LLMClient.callWithVision: at least one image is required');\n }\n\n return this.rateLimiter.execute(async () => {\n const anthropic = new Anthropic({ apiKey: this.providerConfig.apiKey });\n const content = buildVisionMessageContent(userText, images);\n const messages: Anthropic.MessageParam[] = [{ role: 'user', content }];\n\n log.info(\n `[LLMClient:callWithVision] Invoking ${this.provider}/${this.modelName} (images=${images.length})`,\n );\n\n const response = await anthropic.messages.create(\n {\n model: this.modelName,\n max_tokens: maxTokens ?? 4096,\n temperature: temperature ?? 0,\n ...(systemPrompt ? { system: systemPrompt } : {}),\n messages,\n },\n signal ? { signal } : {},\n );\n\n const textContent = response.content.find((c) => c.type === 'text');\n const rawText =\n textContent && 'text' in textContent ? textContent.text : '';\n\n const apiUsage = response.usage;\n const usage: LLMUsage = {\n promptTokens: apiUsage.input_tokens,\n completionTokens: apiUsage.output_tokens,\n totalTokens: apiUsage.input_tokens + apiUsage.output_tokens,\n };\n if (this.tokenTracker) {\n this.tokenTracker.addUsage(usage.promptTokens, usage.completionTokens, {\n provider: this.provider,\n });\n }\n\n const finishReason: LLMFinishReason =\n response.stop_reason === 'end_turn'\n ? 'stop'\n : response.stop_reason === 'max_tokens'\n ? 'length'\n : response.stop_reason === 'tool_use'\n ? 'tool_calls'\n : null;\n\n const data: T = skipSchemaValidation\n ? parseJsonResponse<T>(rawText, undefined)\n : schema\n ? parseJsonResponse<T>(rawText, schema)\n : asGeneric<T>(rawText);\n\n return { data, raw: rawText, finishReason, usage };\n });\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 case 'lmstudio':\n return true; // local background service with a sane default URL, no env var required\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":[]}
@@ -68,7 +68,7 @@ interface ChatCompletionResponse {
68
68
  declare function parseChatCompletionResponse(raw: string): ChatCompletionResponse;
69
69
 
70
70
  type ChatModel = ChatOpenAI | ChatAnthropic;
71
- type LLMProvider = 'openai' | 'deepseek' | 'anthropic' | 'kimi' | 'openrouter' | 'orbgen' | 'masar';
71
+ type LLMProvider = 'openai' | 'deepseek' | 'anthropic' | 'kimi' | 'openrouter' | 'orbgen' | 'masar' | 'lmstudio';
72
72
  interface ProviderConfig {
73
73
  apiKey: string;
74
74
  baseUrl?: string;
@@ -221,6 +221,20 @@ declare const OPENROUTER_MODELS: {
221
221
  readonly KIMI_K2: "moonshotai/kimi-k2";
222
222
  readonly GLM_4_7: "z-ai/glm-4.7";
223
223
  };
224
+ interface ProviderCatalogEntry {
225
+ provider: LLMProvider;
226
+ /** Enumerable models for this provider; empty for env/URL-driven
227
+ * providers (orbgen, masar, lmstudio) with no closed set to choose from. */
228
+ models: string[];
229
+ defaultModel: string;
230
+ }
231
+ /** Single source of truth for "what providers/models does this package
232
+ * support" — consumers (setup wizards, model pickers) should read this
233
+ * instead of importing individual `*_MODELS` consts and re-deriving the
234
+ * provider list themselves. */
235
+ declare const PROVIDER_CATALOG: ProviderCatalogEntry[];
236
+ /** Providers with an enumerable model catalog — safe to offer in a picker. */
237
+ declare function listSelectableProviders(): ProviderCatalogEntry[];
224
238
  declare class LLMClient {
225
239
  private model;
226
240
  private rateLimiter;
@@ -269,6 +283,16 @@ declare class LLMClient {
269
283
  * can override by passing their own `signal`.
270
284
  */
271
285
  private static readonly DEFAULT_RAW_TIMEOUT_MS;
286
+ /**
287
+ * Local/self-hosted providers (e.g. a quantized model on consumer
288
+ * hardware) routinely need far longer than 5 min for a single
289
+ * tool-calling turn under heavy reasoning + a large tool schema — the
290
+ * 5 min default was observed clipping every attempt at exactly
291
+ * 300000ms with no completion in sight, not failing on genuinely slow
292
+ * output. Give them more room; still finite so a truly wedged call
293
+ * doesn't hang forever.
294
+ */
295
+ private static readonly LOCAL_PROVIDER_RAW_TIMEOUT_MS;
272
296
  /**
273
297
  * Wrap a `model.invoke(...)` with start/finish/error logging and a hard
274
298
  * abort-driven timeout. Returns whatever the underlying `invoke` returns.
@@ -454,4 +478,4 @@ declare function createOpenRouterClient(options?: Partial<Omit<LLMClientOptions,
454
478
  */
455
479
  declare function createZhipuClient(options?: Partial<Omit<LLMClientOptions, 'provider'>>): LLMClient;
456
480
 
457
- export { ANTHROPIC_MODELS as A, createOpenRouterClient as B, type CacheAwareLLMCallOptions as C, DEEPSEEK_MODELS as D, createRequirementsClient as E, createZhipuClient as F, getAvailableProvider as G, getSharedLLMClient as H, isProviderAvailable as I, parseChatCompletionResponse as J, KIMI_MODELS as K, type LLMFinishReason as L, resetSharedLLMClient as M, OPENAI_MODELS as O, type ProviderConfig as P, type VisionCallOptions as V, LLMClient as a, type CacheableBlock as b, type ChatCompletionChoice as c, type ChatCompletionMessage as d, type ChatCompletionResponse as e, type ChatCompletionRole as f, type ChatCompletionToolCall as g, type ChatCompletionToolDef as h, type ChatCompletionUsage as i, type LLMCallOptions as j, type LLMClientOptions as k, type LLMProvider as l, type LLMResponse as m, type LLMStreamChunk as n, type LLMStreamOptions as o, type LLMUsage as p, OPENROUTER_MODELS as q, type VisionImageMediaType as r, type VisionImagePart as s, buildVisionMessageContent as t, createAnthropicClient as u, createCreativeClient as v, createDeepSeekClient as w, createFixClient as x, createKimiClient as y, createOpenAIClient as z };
481
+ export { ANTHROPIC_MODELS as A, createKimiClient as B, type CacheAwareLLMCallOptions as C, DEEPSEEK_MODELS as D, createOpenAIClient as E, createOpenRouterClient as F, createRequirementsClient as G, createZhipuClient as H, getAvailableProvider as I, getSharedLLMClient as J, KIMI_MODELS as K, type LLMFinishReason as L, isProviderAvailable as M, listSelectableProviders as N, OPENAI_MODELS as O, PROVIDER_CATALOG as P, parseChatCompletionResponse as Q, resetSharedLLMClient as R, type VisionCallOptions as V, LLMClient as a, type CacheableBlock as b, type ChatCompletionChoice as c, type ChatCompletionMessage as d, type ChatCompletionResponse as e, type ChatCompletionRole as f, type ChatCompletionToolCall as g, type ChatCompletionToolDef as h, type ChatCompletionUsage as i, type LLMCallOptions as j, type LLMClientOptions as k, type LLMProvider as l, type LLMResponse as m, type LLMStreamChunk as n, type LLMStreamOptions as o, type LLMUsage as p, OPENROUTER_MODELS as q, type ProviderCatalogEntry as r, type ProviderConfig as s, type VisionImageMediaType as t, type VisionImagePart as u, buildVisionMessageContent as v, createAnthropicClient as w, createCreativeClient as x, createDeepSeekClient as y, createFixClient as z };
package/dist/client.d.ts CHANGED
@@ -3,5 +3,5 @@ import '@langchain/openai';
3
3
  import '@langchain/anthropic';
4
4
  import '@anthropic-ai/sdk';
5
5
  import 'zod';
6
- export { A as ANTHROPIC_MODELS, C as CacheAwareLLMCallOptions, b as CacheableBlock, D as DEEPSEEK_MODELS, K as KIMI_MODELS, j as LLMCallOptions, a as LLMClient, k as LLMClientOptions, L as LLMFinishReason, l as LLMProvider, m as LLMResponse, n as LLMStreamChunk, o as LLMStreamOptions, p as LLMUsage, O as OPENAI_MODELS, q as OPENROUTER_MODELS, P as ProviderConfig, V as VisionCallOptions, r as VisionImageMediaType, s as VisionImagePart, t as buildVisionMessageContent, u as createAnthropicClient, v as createCreativeClient, w as createDeepSeekClient, x as createFixClient, y as createKimiClient, z as createOpenAIClient, B as createOpenRouterClient, E as createRequirementsClient, F as createZhipuClient, G as getAvailableProvider, H as getSharedLLMClient, I as isProviderAvailable, M as resetSharedLLMClient } from './client-CfCemdbr.js';
6
+ export { A as ANTHROPIC_MODELS, C as CacheAwareLLMCallOptions, b as CacheableBlock, D as DEEPSEEK_MODELS, K as KIMI_MODELS, j as LLMCallOptions, a as LLMClient, k as LLMClientOptions, L as LLMFinishReason, l as LLMProvider, m as LLMResponse, n as LLMStreamChunk, o as LLMStreamOptions, p as LLMUsage, O as OPENAI_MODELS, q as OPENROUTER_MODELS, P as PROVIDER_CATALOG, r as ProviderCatalogEntry, s as ProviderConfig, V as VisionCallOptions, t as VisionImageMediaType, u as VisionImagePart, v as buildVisionMessageContent, w as createAnthropicClient, x as createCreativeClient, y as createDeepSeekClient, z as createFixClient, B as createKimiClient, E as createOpenAIClient, F as createOpenRouterClient, G as createRequirementsClient, H as createZhipuClient, I as getAvailableProvider, J as getSharedLLMClient, M as isProviderAvailable, N as listSelectableProviders, R as resetSharedLLMClient } from './client-CZ4lPDXx.js';
7
7
  import '@almadar/core';
package/dist/client.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  LLMClient,
6
6
  OPENAI_MODELS,
7
7
  OPENROUTER_MODELS,
8
+ PROVIDER_CATALOG,
8
9
  buildVisionMessageContent,
9
10
  createAnthropicClient,
10
11
  createCreativeClient,
@@ -18,8 +19,9 @@ import {
18
19
  getAvailableProvider,
19
20
  getSharedLLMClient,
20
21
  isProviderAvailable,
22
+ listSelectableProviders,
21
23
  resetSharedLLMClient
22
- } from "./chunk-EOZGJJ4J.js";
24
+ } from "./chunk-544CU3RT.js";
23
25
  import "./chunk-P4VCT25B.js";
24
26
  import "./chunk-RPG3SUIB.js";
25
27
  export {
@@ -29,6 +31,7 @@ export {
29
31
  LLMClient,
30
32
  OPENAI_MODELS,
31
33
  OPENROUTER_MODELS,
34
+ PROVIDER_CATALOG,
32
35
  buildVisionMessageContent,
33
36
  createAnthropicClient,
34
37
  createCreativeClient,
@@ -42,6 +45,7 @@ export {
42
45
  getAvailableProvider,
43
46
  getSharedLLMClient,
44
47
  isProviderAvailable,
48
+ listSelectableProviders,
45
49
  resetSharedLLMClient
46
50
  };
47
51
  //# sourceMappingURL=client.js.map
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { L as LLMFinishReason, a as LLMClient } from './client-CfCemdbr.js';
2
- export { A as ANTHROPIC_MODELS, C as CacheAwareLLMCallOptions, b as CacheableBlock, c as ChatCompletionChoice, d as ChatCompletionMessage, e as ChatCompletionResponse, f as ChatCompletionRole, g as ChatCompletionToolCall, h as ChatCompletionToolDef, i as ChatCompletionUsage, D as DEEPSEEK_MODELS, K as KIMI_MODELS, j as LLMCallOptions, k as LLMClientOptions, l as LLMProvider, m as LLMResponse, n as LLMStreamChunk, o as LLMStreamOptions, p as LLMUsage, O as OPENAI_MODELS, q as OPENROUTER_MODELS, P as ProviderConfig, V as VisionCallOptions, r as VisionImageMediaType, s as VisionImagePart, t as buildVisionMessageContent, u as createAnthropicClient, v as createCreativeClient, w as createDeepSeekClient, x as createFixClient, y as createKimiClient, z as createOpenAIClient, B as createOpenRouterClient, E as createRequirementsClient, F as createZhipuClient, G as getAvailableProvider, H as getSharedLLMClient, I as isProviderAvailable, J as parseChatCompletionResponse, M as resetSharedLLMClient } from './client-CfCemdbr.js';
1
+ import { L as LLMFinishReason, a as LLMClient } from './client-CZ4lPDXx.js';
2
+ export { A as ANTHROPIC_MODELS, C as CacheAwareLLMCallOptions, b as CacheableBlock, c as ChatCompletionChoice, d as ChatCompletionMessage, e as ChatCompletionResponse, f as ChatCompletionRole, g as ChatCompletionToolCall, h as ChatCompletionToolDef, i as ChatCompletionUsage, D as DEEPSEEK_MODELS, K as KIMI_MODELS, j as LLMCallOptions, k as LLMClientOptions, l as LLMProvider, m as LLMResponse, n as LLMStreamChunk, o as LLMStreamOptions, p as LLMUsage, O as OPENAI_MODELS, q as OPENROUTER_MODELS, P as PROVIDER_CATALOG, r as ProviderCatalogEntry, s as ProviderConfig, V as VisionCallOptions, t as VisionImageMediaType, u as VisionImagePart, v as buildVisionMessageContent, w as createAnthropicClient, x as createCreativeClient, y as createDeepSeekClient, z as createFixClient, B as createKimiClient, E as createOpenAIClient, F as createOpenRouterClient, G as createRequirementsClient, H as createZhipuClient, I as getAvailableProvider, J as getSharedLLMClient, M as isProviderAvailable, N as listSelectableProviders, Q as parseChatCompletionResponse, R as resetSharedLLMClient } from './client-CZ4lPDXx.js';
3
3
  export { a as RateLimiter, R as RateLimiterOptions, b as TokenTracker, T as TokenUsage, g as getGlobalRateLimiter, c as getGlobalTokenTracker, r as resetGlobalRateLimiter, d as resetGlobalTokenTracker } from './rate-limiter-CXaf8aAy.js';
4
4
  export { autoCloseJson, extractJsonFromText, isValidJson, parseJsonResponse, safeParseJson } from './json-parser.js';
5
5
  import { z } from 'zod';
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  LLMClient,
6
6
  OPENAI_MODELS,
7
7
  OPENROUTER_MODELS,
8
+ PROVIDER_CATALOG,
8
9
  buildVisionMessageContent,
9
10
  createAnthropicClient,
10
11
  createCreativeClient,
@@ -18,9 +19,10 @@ import {
18
19
  getAvailableProvider,
19
20
  getSharedLLMClient,
20
21
  isProviderAvailable,
22
+ listSelectableProviders,
21
23
  parseChatCompletionResponse,
22
24
  resetSharedLLMClient
23
- } from "./chunk-EOZGJJ4J.js";
25
+ } from "./chunk-544CU3RT.js";
24
26
  import {
25
27
  autoCloseJson,
26
28
  extractJsonFromText,
@@ -586,6 +588,7 @@ export {
586
588
  MasarProvider,
587
589
  OPENAI_MODELS,
588
590
  OPENROUTER_MODELS,
591
+ PROVIDER_CATALOG,
589
592
  RateLimiter,
590
593
  STRUCTURED_OUTPUT_MODELS,
591
594
  StructuredOutputClient,
@@ -617,6 +620,7 @@ export {
617
620
  isProviderAvailable,
618
621
  isStructuredOutputAvailable,
619
622
  isValidJson,
623
+ listSelectableProviders,
620
624
  mergeResponses,
621
625
  parseChatCompletionResponse,
622
626
  parseJsonResponse,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/embedding-client.ts","../src/truncation-detector.ts","../src/continuation.ts"],"sourcesContent":["/**\n * Embedding Client\n *\n * Single-purpose client for text embeddings. Used by `@almadar-io/agent`'s\n * cosine-similarity catalog retrieval (rank organisms + atoms against the\n * user's request before rendering Stage A's prompt) and by\n * `@almadar/std`'s publish-time embedding bake step.\n *\n * Providers:\n * - `openai` (default model `text-embedding-3-small`, 1536-d) — requires\n * `OPENAI_API_KEY`.\n * - `openrouter` (default model `baai/bge-base-en-v1.5`, 768-d) — requires\n * `OPEN_ROUTER_API_KEY`. Same OpenAI-compatible request shape, just a\n * different base URL.\n *\n * Both providers return the same response envelope (`{data:[{embedding,index}]}`),\n * so the request/response code is shared.\n *\n * @packageDocumentation\n */\n\nexport type EmbeddingProvider = 'openai' | 'openrouter';\n\nexport interface EmbeddingClientOptions {\n provider: EmbeddingProvider;\n /** Defaults: openai → text-embedding-3-small, openrouter → baai/bge-base-en-v1.5. */\n model?: string;\n /** Override API key. Defaults to provider-specific env var. */\n apiKey?: string;\n /** Override base URL. Defaults to provider canonical endpoint. */\n baseUrl?: string;\n}\n\nexport interface EmbeddingUsage {\n promptTokens: number;\n totalTokens: number;\n}\n\nexport interface EmbeddingResult {\n embedding: readonly number[];\n usage: EmbeddingUsage;\n}\n\nexport interface EmbeddingBatchResult {\n embeddings: readonly (readonly number[])[];\n usage: EmbeddingUsage;\n}\n\ninterface OpenAIEmbeddingApiResponse {\n data: Array<{ embedding: number[]; index: number }>;\n usage?: { prompt_tokens?: number; total_tokens?: number };\n /** OpenRouter reports provider hiccups as HTTP 200 with an error envelope and no `data`. */\n error?: { message?: string; code?: number | string };\n}\n\n/** Transient-failure retry policy: attempts and backoff (ms) between them. */\nconst EMBED_MAX_ATTEMPTS = 3;\nconst EMBED_RETRY_BACKOFF_MS = [500, 2000];\n\n/** Provider rejects an input that exceeds the model's context window (bge: 512 tokens). */\nconst TOKEN_OVERFLOW_RE =\n /context length|maximum input length|reduce the length of the input|input_tokens/i;\n\nconst DEFAULT_MODELS: Record<EmbeddingProvider, string> = {\n openai: 'text-embedding-3-small',\n openrouter: 'baai/bge-base-en-v1.5',\n};\n\nconst DEFAULT_BASE_URLS: Record<EmbeddingProvider, string> = {\n openai: 'https://api.openai.com/v1',\n openrouter: 'https://openrouter.ai/api/v1',\n};\n\nconst API_KEY_ENV_VARS: Record<EmbeddingProvider, string> = {\n openai: 'OPENAI_API_KEY',\n openrouter: 'OPEN_ROUTER_API_KEY',\n};\n\nexport class EmbeddingClient {\n private readonly provider: EmbeddingProvider;\n private readonly model: string;\n private readonly apiKey: string;\n private readonly baseUrl: string;\n\n constructor(opts: EmbeddingClientOptions) {\n this.provider = opts.provider;\n this.model = opts.model ?? DEFAULT_MODELS[opts.provider];\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URLS[opts.provider]).replace(/\\/$/, '');\n\n const apiKey = opts.apiKey ?? this.resolveApiKey();\n if (!apiKey) {\n throw new Error(\n `EmbeddingClient: API key for provider '${this.provider}' not found. ` +\n `Set ${API_KEY_ENV_VARS[this.provider]} in your environment or pass options.apiKey.`,\n );\n }\n this.apiKey = apiKey;\n }\n\n /** Embed a single text. */\n async embed(text: string): Promise<EmbeddingResult> {\n const batch = await this.embedBatch([text]);\n const embedding = batch.embeddings[0];\n if (!embedding) {\n throw new Error('EmbeddingClient.embed: provider returned no embedding');\n }\n return { embedding, usage: batch.usage };\n }\n\n /**\n * Embed a batch of texts. Used by `@almadar/std`'s publish-time\n * `build-embeddings.ts` script — one batch call for the full catalog\n * (≈190 entries) rather than per-entry requests.\n */\n async embedBatch(texts: ReadonlyArray<string>): Promise<EmbeddingBatchResult> {\n if (texts.length === 0) {\n return { embeddings: [], usage: { promptTokens: 0, totalTokens: 0 } };\n }\n // bge-base-en-v1.5 has a 512-token context limit. Chars-per-token varies\n // with density (~4 for typical English, ~2.5-3 for dense technical/code\n // text), so no fixed char cap can guarantee a fit. Start at 2000 chars and,\n // when the provider rejects an input for exceeding the context window,\n // halve the cap and retry — the provider's own token count is the source\n // of truth, not a chars-per-token guess.\n let charCap = 2000;\n let lastError: Error = new Error('EmbeddingClient: unreachable');\n for (let attempt = 1; attempt <= EMBED_MAX_ATTEMPTS; attempt++) {\n const trimmed = texts.map((t) => (t.length > charCap ? t.slice(0, charCap) : t));\n try {\n return await this.embedBatchOnce(texts.length, trimmed);\n } catch (e) {\n lastError = e instanceof Error ? e : new Error(String(e));\n if (TOKEN_OVERFLOW_RE.test(lastError.message) && charCap > 500) {\n charCap = Math.floor(charCap / 2);\n continue;\n }\n const backoff = EMBED_RETRY_BACKOFF_MS[attempt - 1];\n if (backoff === undefined) break;\n await new Promise((r) => setTimeout(r, backoff));\n }\n }\n throw lastError;\n }\n\n private async embedBatchOnce(\n inputCount: number,\n trimmed: ReadonlyArray<string>,\n ): Promise<EmbeddingBatchResult> {\n const response = await fetch(`${this.baseUrl}/embeddings`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({ model: this.model, input: trimmed }),\n });\n if (!response.ok) {\n const errorText = await response.text().catch(() => '<no body>');\n throw new Error(\n `EmbeddingClient: ${this.provider} returned ${response.status} ${response.statusText}: ${errorText}`,\n );\n }\n const rawBody = await response.text();\n let body: OpenAIEmbeddingApiResponse;\n try {\n body = JSON.parse(rawBody) as OpenAIEmbeddingApiResponse;\n } catch {\n throw new Error(\n `EmbeddingClient: ${this.provider} returned unparseable body: ${rawBody}`,\n );\n }\n if (!Array.isArray(body.data)) {\n // OpenRouter reports provider hiccups as 200 + `{error}` with no data —\n // surface the real body instead of a blind \"missing data\" (G9: this\n // error was undiagnosable for weeks because the body was discarded).\n const detail = body.error?.message ?? rawBody;\n throw new Error(\n `EmbeddingClient: ${this.provider} response has no \\`data\\` array: ${detail}`,\n );\n }\n const ordered = [...body.data].sort((a, b) => a.index - b.index);\n if (ordered.length !== inputCount) {\n throw new Error(\n `EmbeddingClient: provider returned ${ordered.length} embeddings for ${inputCount} inputs`,\n );\n }\n return {\n embeddings: ordered.map((d) => d.embedding),\n usage: {\n promptTokens: body.usage?.prompt_tokens ?? 0,\n totalTokens: body.usage?.total_tokens ?? 0,\n },\n };\n }\n\n private resolveApiKey(): string | undefined {\n return process.env[API_KEY_ENV_VARS[this.provider]];\n }\n}\n\n/**\n * Cosine similarity between two equal-length vectors. Returns a value in\n * [-1, 1]; higher means more similar. Used by `@almadar-io/agent`'s\n * catalog retrieval to rank organism/atom descriptions against the user\n * request embedding.\n *\n * Defensive: returns 0 when either vector is the zero vector (would\n * otherwise divide by zero).\n */\nexport function cosineSimilarity(a: ReadonlyArray<number>, b: ReadonlyArray<number>): number {\n if (a.length !== b.length) {\n throw new Error(`cosineSimilarity: vector length mismatch (${a.length} vs ${b.length})`);\n }\n let dot = 0;\n let normA = 0;\n let normB = 0;\n for (let i = 0; i < a.length; i++) {\n const av = a[i];\n const bv = b[i];\n dot += av * bv;\n normA += av * av;\n normB += bv * bv;\n }\n if (normA === 0 || normB === 0) return 0;\n return dot / (Math.sqrt(normA) * Math.sqrt(normB));\n}\n","/**\n * Truncation Detector\n *\n * Utilities for detecting when LLM output has been truncated and\n * extracting usable content from partial responses.\n *\n * @packageDocumentation\n */\n\nimport type { LLMFinishReason } from './client.js';\nimport { autoCloseJson } from './json-parser.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type TruncationReason =\n | 'finish_reason'\n | 'json_incomplete'\n | 'bracket_mismatch'\n | 'none';\n\nexport interface TruncationResult {\n isTruncated: boolean;\n reason: TruncationReason;\n partialContent?: string;\n lastCompleteElement?: unknown;\n missingCloseBrackets?: number;\n missingCloseBraces?: number;\n}\n\n// ============================================================================\n// Main Detection Function\n// ============================================================================\n\n/**\n * Detect if an LLM response has been truncated.\n *\n * Analyzes the response content and finish reason to determine if\n * the output was cut off due to token limits or other issues.\n *\n * @param {string} response - The LLM response text\n * @param {LLMFinishReason} finishReason - The finish reason from the LLM\n * @returns {TruncationResult} Detection result with truncation details\n */\nexport function detectTruncation(\n response: string,\n finishReason: LLMFinishReason,\n): TruncationResult {\n if (finishReason === 'length') {\n const bracketInfo = countBrackets(response);\n return {\n isTruncated: true,\n reason: 'finish_reason',\n partialContent: response,\n lastCompleteElement: findLastCompleteElement(response),\n missingCloseBrackets: bracketInfo.missingCloseBrackets,\n missingCloseBraces: bracketInfo.missingCloseBraces,\n };\n }\n\n try {\n JSON.parse(response);\n return { isTruncated: false, reason: 'none' };\n } catch {\n // JSON is invalid, check if due to truncation\n }\n\n if (finishReason === 'stop' || finishReason === null) {\n const trimmed = response.trim();\n\n const isMidContent =\n trimmed.endsWith(',') ||\n trimmed.endsWith(':') ||\n trimmed.endsWith('\": ') ||\n /:\\s*$/.test(trimmed) ||\n /,\\s*$/.test(trimmed);\n\n if (isMidContent) {\n const bracketInfo = countBrackets(response);\n return {\n isTruncated: true,\n reason: 'json_incomplete',\n partialContent: response,\n lastCompleteElement: findLastCompleteElement(response),\n missingCloseBrackets: bracketInfo.missingCloseBrackets,\n missingCloseBraces: bracketInfo.missingCloseBraces,\n };\n }\n\n try {\n const closed = autoCloseJson(trimmed);\n JSON.parse(closed);\n return { isTruncated: false, reason: 'none' };\n } catch {\n return { isTruncated: false, reason: 'none' };\n }\n }\n\n const bracketInfo = countBrackets(response);\n if (\n bracketInfo.missingCloseBrackets > 0 ||\n bracketInfo.missingCloseBraces > 0\n ) {\n return {\n isTruncated: true,\n reason: 'bracket_mismatch',\n partialContent: response,\n lastCompleteElement: findLastCompleteElement(response),\n missingCloseBrackets: bracketInfo.missingCloseBrackets,\n missingCloseBraces: bracketInfo.missingCloseBraces,\n };\n }\n\n return { isTruncated: false, reason: 'none' };\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\nfunction countBrackets(json: string): {\n openBrackets: number;\n closeBrackets: number;\n openBraces: number;\n closeBraces: number;\n missingCloseBrackets: number;\n missingCloseBraces: number;\n} {\n let inString = false;\n let escaped = false;\n let openBrackets = 0;\n let closeBrackets = 0;\n let openBraces = 0;\n let closeBraces = 0;\n\n for (const char of json) {\n if (escaped) {\n escaped = false;\n continue;\n }\n if (char === '\\\\' && inString) {\n escaped = true;\n continue;\n }\n if (char === '\"') {\n inString = !inString;\n continue;\n }\n if (inString) continue;\n\n switch (char) {\n case '[':\n openBrackets++;\n break;\n case ']':\n closeBrackets++;\n break;\n case '{':\n openBraces++;\n break;\n case '}':\n closeBraces++;\n break;\n }\n }\n\n return {\n openBrackets,\n closeBrackets,\n openBraces,\n closeBraces,\n missingCloseBrackets: Math.max(0, openBrackets - closeBrackets),\n missingCloseBraces: Math.max(0, openBraces - closeBraces),\n };\n}\n\nexport function findLastCompleteElement(json: string): unknown | null {\n const autoClosed = autoCloseJson(json);\n try {\n return JSON.parse(autoClosed);\n } catch {\n // Auto-close didn't work\n }\n\n const trimmed = json.trim();\n\n if (trimmed.startsWith('[')) {\n const lastCompleteIndex = findLastCompleteArrayElement(trimmed);\n if (lastCompleteIndex > 0) {\n const subset = trimmed.substring(0, lastCompleteIndex) + ']';\n try {\n return JSON.parse(subset);\n } catch {\n // Continue\n }\n }\n }\n\n if (trimmed.startsWith('{')) {\n const closed = autoCloseJson(trimmed);\n try {\n return JSON.parse(closed);\n } catch {\n const lastCompleteIndex = findLastCompleteObjectProperty(trimmed);\n if (lastCompleteIndex > 0) {\n const subset = trimmed.substring(0, lastCompleteIndex) + '}';\n try {\n return JSON.parse(subset);\n } catch {\n // Give up\n }\n }\n }\n }\n\n return null;\n}\n\nfunction findLastCompleteArrayElement(json: string): number {\n let depth = 0;\n let inString = false;\n let escaped = false;\n let lastCompleteElementEnd = -1;\n\n for (let i = 0; i < json.length; i++) {\n const char = json[i];\n\n if (escaped) {\n escaped = false;\n continue;\n }\n if (char === '\\\\' && inString) {\n escaped = true;\n continue;\n }\n if (char === '\"') {\n inString = !inString;\n continue;\n }\n if (inString) continue;\n\n if (char === '[' || char === '{') {\n depth++;\n } else if (char === ']' || char === '}') {\n depth--;\n if (depth === 1) {\n lastCompleteElementEnd = i + 1;\n }\n } else if (char === ',' && depth === 1) {\n lastCompleteElementEnd = i;\n }\n }\n\n return lastCompleteElementEnd > 0 ? lastCompleteElementEnd : -1;\n}\n\nfunction findLastCompleteObjectProperty(json: string): number {\n let depth = 0;\n let inString = false;\n let escaped = false;\n let lastCommaIndex = -1;\n\n for (let i = 0; i < json.length; i++) {\n const char = json[i];\n\n if (escaped) {\n escaped = false;\n continue;\n }\n if (char === '\\\\' && inString) {\n escaped = true;\n continue;\n }\n if (char === '\"') {\n inString = !inString;\n continue;\n }\n if (inString) continue;\n\n if (char === '[' || char === '{') {\n depth++;\n } else if (char === ']' || char === '}') {\n depth--;\n } else if (char === ',' && depth === 1) {\n lastCommaIndex = i;\n }\n }\n\n return lastCommaIndex > 0 ? lastCommaIndex : -1;\n}\n\nexport function isLikelyTruncated(content: string): boolean {\n const trimmed = content.trim();\n if (!trimmed) return false;\n\n const brackets = countBrackets(trimmed);\n if (\n brackets.missingCloseBrackets > 0 ||\n brackets.missingCloseBraces > 0\n ) {\n return true;\n }\n\n const abruptEndings = [\n /,\\s*$/,\n /:\\s*$/,\n /\"\\s*:\\s*$/,\n /\\[\\s*$/,\n /{\\s*$/,\n ];\n\n for (const pattern of abruptEndings) {\n if (pattern.test(trimmed)) return true;\n }\n\n return false;\n}\n","/**\n * LLM Continuation Utility\n *\n * Handles truncated LLM responses with automatic continuation.\n * - Detects truncation via finish_reason and JSON structure\n * - Automatically continues with full context\n * - Merges partial and continuation responses\n * - Salvages partial data if max continuations reached\n *\n * @packageDocumentation\n */\n\nimport { z } from 'zod';\nimport { LLMClient, type LLMFinishReason } from './client.js';\nimport { detectTruncation } from './truncation-detector.js';\nimport { extractJsonFromText, autoCloseJson, isValidJson } from './json-parser.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ContinuationOptions<T> {\n client: LLMClient;\n systemPrompt: string;\n userPrompt: string;\n schema?: z.ZodSchema<T>;\n maxTokens?: number;\n maxContinuations?: number;\n maxRetries?: number;\n buildContinuationPrompt: (\n partialResponse: string,\n attempt: number,\n ) => string;\n continuationSystemPrompt?: string;\n}\n\nexport interface ContinuationResult<T> {\n data: T;\n raw: string;\n continuationCount: number;\n warnings: string[];\n wasSalvaged: boolean;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst DEFAULT_MAX_TOKENS = 8192;\nconst DEFAULT_MAX_CONTINUATIONS = 3;\n\n/**\n * Default continuation system prompt.\n * Used when no custom continuationSystemPrompt is provided.\n */\nconst DEFAULT_CONTINUATION_SYSTEM_PROMPT = `You are a JSON continuation assistant. Your ONLY job is to continue generating JSON from where the previous response was truncated.\n\nRules:\n1. Continue from EXACTLY where the previous output stopped\n2. Do NOT repeat any content already generated\n3. Complete the JSON structure properly with all closing brackets\n4. Do NOT wrap in markdown code blocks\n5. Output ONLY the continuation JSON, nothing else`;\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Merge a previous partial response with a continuation.\n *\n * Handles JSON structure merging, removing markdown code blocks,\n * and ensuring proper comma separation between merged parts.\n *\n * @param {string} previous - The previous partial response\n * @param {string} continuation - The continuation to merge\n * @returns {string} The merged response\n */\nexport function mergeResponses(\n previous: string,\n continuation: string,\n): string {\n const trimmedPrev = previous.trimEnd();\n const trimmedCont = continuation.trimStart();\n\n let cleanedCont = trimmedCont\n .replace(/^```json?\\s*/i, '')\n .replace(/```\\s*$/i, '')\n .trim();\n\n if (cleanedCont.startsWith('{')) {\n try {\n const contParsed = JSON.parse(autoCloseJson(cleanedCont));\n const keys = Object.keys(contParsed);\n if (keys.length === 1 && Array.isArray(contParsed[keys[0]])) {\n cleanedCont = contParsed[keys[0]]\n .map((item: unknown) => JSON.stringify(item))\n .join(',\\n');\n }\n } catch {\n // Continue with original cleaning\n }\n }\n\n if (cleanedCont.startsWith('}') || cleanedCont.startsWith(']')) {\n return trimmedPrev + cleanedCont;\n }\n\n const prevEndsWithValue = /[\\}\\]\\\"\\d]$/.test(trimmedPrev);\n const contStartsWithValue = /^[\\{\\[\\\"]/.test(cleanedCont);\n\n if (prevEndsWithValue && contStartsWithValue) {\n return trimmedPrev + ',\\n' + cleanedCont;\n }\n\n return trimmedPrev + cleanedCont;\n}\n\nexport function salvagePartialResponse<T>(rawResponse: string): T | null {\n console.warn('[Continuation] Attempting to salvage partial response');\n\n try {\n const cleanedResponse = extractJsonFromText(rawResponse) || rawResponse;\n const closed = autoCloseJson(cleanedResponse);\n const parsed = JSON.parse(closed) as T;\n console.log('[Continuation] Successfully salvaged partial response');\n return parsed;\n } catch (error) {\n console.error('[Continuation] Could not salvage response:', error);\n }\n\n return null;\n}\n\n// ============================================================================\n// Main Function\n// ============================================================================\n\n/**\n * Call an LLM with automatic continuation handling.\n *\n * Manages token limits by detecting truncation and making continuation\n * calls until the complete response is received or max continuations reached.\n *\n * @template T - Expected response type\n * @param {ContinuationOptions<T>} options - Continuation call options\n * @returns {Promise<ContinuationResult<T>>} Result with data and continuation metadata\n */\nexport async function callWithContinuation<T>(\n options: ContinuationOptions<T>,\n): Promise<ContinuationResult<T>> {\n const {\n client,\n systemPrompt,\n userPrompt,\n schema,\n maxTokens = DEFAULT_MAX_TOKENS,\n maxContinuations = DEFAULT_MAX_CONTINUATIONS,\n buildContinuationPrompt,\n continuationSystemPrompt = DEFAULT_CONTINUATION_SYSTEM_PROMPT,\n } = options;\n\n let rawResponse = '';\n let continuationCount = 0;\n const warnings: string[] = [];\n let wasSalvaged = false;\n\n console.log('[Continuation] Starting LLM call with continuation support');\n console.log(\n `[Continuation] Max tokens: ${maxTokens}, Max continuations: ${maxContinuations}`,\n );\n\n try {\n const response = await client.callRawWithMetadata({\n systemPrompt,\n userPrompt,\n maxTokens,\n });\n\n rawResponse = extractJsonFromText(response.raw) || response.raw;\n\n console.log(\n `[Continuation] Initial response: ${rawResponse.length} chars, finish_reason: ${response.finishReason}`,\n );\n\n let truncation = detectTruncation(rawResponse, response.finishReason);\n\n while (truncation.isTruncated && continuationCount < maxContinuations) {\n continuationCount++;\n const warningMsg = `Response truncated (${truncation.reason}), continuing (attempt ${continuationCount}/${maxContinuations})`;\n console.log(`[Continuation] ${warningMsg}`);\n warnings.push(warningMsg);\n\n const contPrompt = buildContinuationPrompt(\n rawResponse,\n continuationCount,\n );\n\n const contResponse = await client.callRawWithMetadata({\n systemPrompt: continuationSystemPrompt,\n userPrompt: contPrompt,\n maxTokens,\n });\n\n console.log(\n `[Continuation] Continuation response: ${contResponse.raw.length} chars, finish_reason: ${contResponse.finishReason}`,\n );\n\n const cleanedContResponse =\n extractJsonFromText(contResponse.raw) || contResponse.raw;\n rawResponse = mergeResponses(rawResponse, cleanedContResponse);\n\n truncation = detectTruncation(rawResponse, contResponse.finishReason);\n }\n\n if (\n continuationCount >= maxContinuations &&\n truncation.isTruncated\n ) {\n console.warn(\n `[Continuation] Reached max continuations (${maxContinuations}), attempting to salvage...`,\n );\n warnings.push(\n `Reached max continuations - some content may be incomplete`,\n );\n wasSalvaged = true;\n }\n\n const cleanedResponse =\n extractJsonFromText(rawResponse) || rawResponse;\n let data: T;\n\n try {\n if (isValidJson(cleanedResponse)) {\n data = JSON.parse(cleanedResponse) as T;\n } else {\n const closed = autoCloseJson(cleanedResponse);\n data = JSON.parse(closed) as T;\n if (!wasSalvaged) {\n warnings.push('Response required auto-closing of JSON brackets');\n }\n }\n } catch (parseError) {\n const salvaged = salvagePartialResponse<T>(cleanedResponse);\n if (salvaged) {\n data = salvaged;\n wasSalvaged = true;\n warnings.push('Response was salvaged from partial data');\n } else {\n throw new Error(\n `Failed to parse response after ${continuationCount} continuations: ${parseError}`,\n );\n }\n }\n\n if (schema) {\n try {\n data = schema.parse(data);\n } catch (validationError) {\n console.warn(\n '[Continuation] Schema validation failed:',\n validationError,\n );\n warnings.push(`Schema validation issue: ${validationError}`);\n }\n }\n\n console.log(\n `[Continuation] Complete. Continuations: ${continuationCount}, Warnings: ${warnings.length}`,\n );\n\n return {\n data,\n raw: rawResponse,\n continuationCount,\n warnings,\n wasSalvaged,\n };\n } catch (error) {\n console.error('[Continuation] Error during LLM call:', error);\n throw error;\n }\n}\n\n/**\n * Build a generic continuation prompt.\n *\n * Creates a prompt that instructs the LLM to continue from where\n * a previous truncated response left off.\n *\n * @param {string} context - Original context/prompt\n * @param {string} partialResponse - The partial response generated so far\n * @param {number} attempt - Current continuation attempt number\n * @param {number} [maxAttempts] - Maximum allowed continuation attempts\n * @returns {string} The formatted continuation prompt\n */\nexport function buildGenericContinuationPrompt(\n context: string,\n partialResponse: string,\n attempt: number,\n maxAttempts: number = DEFAULT_MAX_CONTINUATIONS,\n): string {\n return `## CONTINUATION REQUEST (Attempt ${attempt}/${maxAttempts})\n\nYour previous response was truncated. Continue generating from where you left off.\n\n### ORIGINAL CONTEXT\n${context}\n\n### WHAT YOU GENERATED SO FAR\n\\`\\`\\`json\n${partialResponse}\n\\`\\`\\`\n\n### INSTRUCTIONS\n1. Continue from EXACTLY where the response was cut off\n2. Do NOT repeat any content already generated\n3. Complete the JSON structure properly\n4. Do NOT wrap your response in markdown code blocks\n\nContinue generating now:`;\n}\n"],"mappings":"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","names":["bracketInfo"]}
1
+ {"version":3,"sources":["../src/embedding-client.ts","../src/truncation-detector.ts","../src/continuation.ts"],"sourcesContent":["/**\n * Embedding Client\n *\n * Single-purpose client for text embeddings. Used by `@almadar-io/agent`'s\n * cosine-similarity catalog retrieval (rank organisms + atoms against the\n * user's request before rendering Stage A's prompt) and by\n * `@almadar/std`'s publish-time embedding bake step.\n *\n * Providers:\n * - `openai` (default model `text-embedding-3-small`, 1536-d) — requires\n * `OPENAI_API_KEY`.\n * - `openrouter` (default model `baai/bge-base-en-v1.5`, 768-d) — requires\n * `OPEN_ROUTER_API_KEY`. Same OpenAI-compatible request shape, just a\n * different base URL.\n *\n * Both providers return the same response envelope (`{data:[{embedding,index}]}`),\n * so the request/response code is shared.\n *\n * @packageDocumentation\n */\n\nexport type EmbeddingProvider = 'openai' | 'openrouter';\n\nexport interface EmbeddingClientOptions {\n provider: EmbeddingProvider;\n /** Defaults: openai → text-embedding-3-small, openrouter → baai/bge-base-en-v1.5. */\n model?: string;\n /** Override API key. Defaults to provider-specific env var. */\n apiKey?: string;\n /** Override base URL. Defaults to provider canonical endpoint. */\n baseUrl?: string;\n}\n\nexport interface EmbeddingUsage {\n promptTokens: number;\n totalTokens: number;\n}\n\nexport interface EmbeddingResult {\n embedding: readonly number[];\n usage: EmbeddingUsage;\n}\n\nexport interface EmbeddingBatchResult {\n embeddings: readonly (readonly number[])[];\n usage: EmbeddingUsage;\n}\n\ninterface OpenAIEmbeddingApiResponse {\n data: Array<{ embedding: number[]; index: number }>;\n usage?: { prompt_tokens?: number; total_tokens?: number };\n /** OpenRouter reports provider hiccups as HTTP 200 with an error envelope and no `data`. */\n error?: { message?: string; code?: number | string };\n}\n\n/** Transient-failure retry policy: attempts and backoff (ms) between them. */\nconst EMBED_MAX_ATTEMPTS = 3;\nconst EMBED_RETRY_BACKOFF_MS = [500, 2000];\n\n/** Provider rejects an input that exceeds the model's context window (bge: 512 tokens). */\nconst TOKEN_OVERFLOW_RE =\n /context length|maximum input length|reduce the length of the input|input_tokens/i;\n\nconst DEFAULT_MODELS: Record<EmbeddingProvider, string> = {\n openai: 'text-embedding-3-small',\n openrouter: 'baai/bge-base-en-v1.5',\n};\n\nconst DEFAULT_BASE_URLS: Record<EmbeddingProvider, string> = {\n openai: 'https://api.openai.com/v1',\n openrouter: 'https://openrouter.ai/api/v1',\n};\n\nconst API_KEY_ENV_VARS: Record<EmbeddingProvider, string> = {\n openai: 'OPENAI_API_KEY',\n openrouter: 'OPEN_ROUTER_API_KEY',\n};\n\nexport class EmbeddingClient {\n private readonly provider: EmbeddingProvider;\n private readonly model: string;\n private readonly apiKey: string;\n private readonly baseUrl: string;\n\n constructor(opts: EmbeddingClientOptions) {\n this.provider = opts.provider;\n this.model = opts.model ?? DEFAULT_MODELS[opts.provider];\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URLS[opts.provider]).replace(/\\/$/, '');\n\n const apiKey = opts.apiKey ?? this.resolveApiKey();\n if (!apiKey) {\n throw new Error(\n `EmbeddingClient: API key for provider '${this.provider}' not found. ` +\n `Set ${API_KEY_ENV_VARS[this.provider]} in your environment or pass options.apiKey.`,\n );\n }\n this.apiKey = apiKey;\n }\n\n /** Embed a single text. */\n async embed(text: string): Promise<EmbeddingResult> {\n const batch = await this.embedBatch([text]);\n const embedding = batch.embeddings[0];\n if (!embedding) {\n throw new Error('EmbeddingClient.embed: provider returned no embedding');\n }\n return { embedding, usage: batch.usage };\n }\n\n /**\n * Embed a batch of texts. Used by `@almadar/std`'s publish-time\n * `build-embeddings.ts` script — one batch call for the full catalog\n * (≈190 entries) rather than per-entry requests.\n */\n async embedBatch(texts: ReadonlyArray<string>): Promise<EmbeddingBatchResult> {\n if (texts.length === 0) {\n return { embeddings: [], usage: { promptTokens: 0, totalTokens: 0 } };\n }\n // bge-base-en-v1.5 has a 512-token context limit. Chars-per-token varies\n // with density (~4 for typical English, ~2.5-3 for dense technical/code\n // text), so no fixed char cap can guarantee a fit. Start at 2000 chars and,\n // when the provider rejects an input for exceeding the context window,\n // halve the cap and retry — the provider's own token count is the source\n // of truth, not a chars-per-token guess.\n let charCap = 2000;\n let lastError: Error = new Error('EmbeddingClient: unreachable');\n for (let attempt = 1; attempt <= EMBED_MAX_ATTEMPTS; attempt++) {\n const trimmed = texts.map((t) => (t.length > charCap ? t.slice(0, charCap) : t));\n try {\n return await this.embedBatchOnce(texts.length, trimmed);\n } catch (e) {\n lastError = e instanceof Error ? e : new Error(String(e));\n if (TOKEN_OVERFLOW_RE.test(lastError.message) && charCap > 500) {\n charCap = Math.floor(charCap / 2);\n continue;\n }\n const backoff = EMBED_RETRY_BACKOFF_MS[attempt - 1];\n if (backoff === undefined) break;\n await new Promise((r) => setTimeout(r, backoff));\n }\n }\n throw lastError;\n }\n\n private async embedBatchOnce(\n inputCount: number,\n trimmed: ReadonlyArray<string>,\n ): Promise<EmbeddingBatchResult> {\n const response = await fetch(`${this.baseUrl}/embeddings`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({ model: this.model, input: trimmed }),\n });\n if (!response.ok) {\n const errorText = await response.text().catch(() => '<no body>');\n throw new Error(\n `EmbeddingClient: ${this.provider} returned ${response.status} ${response.statusText}: ${errorText}`,\n );\n }\n const rawBody = await response.text();\n let body: OpenAIEmbeddingApiResponse;\n try {\n body = JSON.parse(rawBody) as OpenAIEmbeddingApiResponse;\n } catch {\n throw new Error(\n `EmbeddingClient: ${this.provider} returned unparseable body: ${rawBody}`,\n );\n }\n if (!Array.isArray(body.data)) {\n // OpenRouter reports provider hiccups as 200 + `{error}` with no data —\n // surface the real body instead of a blind \"missing data\" (G9: this\n // error was undiagnosable for weeks because the body was discarded).\n const detail = body.error?.message ?? rawBody;\n throw new Error(\n `EmbeddingClient: ${this.provider} response has no \\`data\\` array: ${detail}`,\n );\n }\n const ordered = [...body.data].sort((a, b) => a.index - b.index);\n if (ordered.length !== inputCount) {\n throw new Error(\n `EmbeddingClient: provider returned ${ordered.length} embeddings for ${inputCount} inputs`,\n );\n }\n return {\n embeddings: ordered.map((d) => d.embedding),\n usage: {\n promptTokens: body.usage?.prompt_tokens ?? 0,\n totalTokens: body.usage?.total_tokens ?? 0,\n },\n };\n }\n\n private resolveApiKey(): string | undefined {\n return process.env[API_KEY_ENV_VARS[this.provider]];\n }\n}\n\n/**\n * Cosine similarity between two equal-length vectors. Returns a value in\n * [-1, 1]; higher means more similar. Used by `@almadar-io/agent`'s\n * catalog retrieval to rank organism/atom descriptions against the user\n * request embedding.\n *\n * Defensive: returns 0 when either vector is the zero vector (would\n * otherwise divide by zero).\n */\nexport function cosineSimilarity(a: ReadonlyArray<number>, b: ReadonlyArray<number>): number {\n if (a.length !== b.length) {\n throw new Error(`cosineSimilarity: vector length mismatch (${a.length} vs ${b.length})`);\n }\n let dot = 0;\n let normA = 0;\n let normB = 0;\n for (let i = 0; i < a.length; i++) {\n const av = a[i];\n const bv = b[i];\n dot += av * bv;\n normA += av * av;\n normB += bv * bv;\n }\n if (normA === 0 || normB === 0) return 0;\n return dot / (Math.sqrt(normA) * Math.sqrt(normB));\n}\n","/**\n * Truncation Detector\n *\n * Utilities for detecting when LLM output has been truncated and\n * extracting usable content from partial responses.\n *\n * @packageDocumentation\n */\n\nimport type { LLMFinishReason } from './client.js';\nimport { autoCloseJson } from './json-parser.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type TruncationReason =\n | 'finish_reason'\n | 'json_incomplete'\n | 'bracket_mismatch'\n | 'none';\n\nexport interface TruncationResult {\n isTruncated: boolean;\n reason: TruncationReason;\n partialContent?: string;\n lastCompleteElement?: unknown;\n missingCloseBrackets?: number;\n missingCloseBraces?: number;\n}\n\n// ============================================================================\n// Main Detection Function\n// ============================================================================\n\n/**\n * Detect if an LLM response has been truncated.\n *\n * Analyzes the response content and finish reason to determine if\n * the output was cut off due to token limits or other issues.\n *\n * @param {string} response - The LLM response text\n * @param {LLMFinishReason} finishReason - The finish reason from the LLM\n * @returns {TruncationResult} Detection result with truncation details\n */\nexport function detectTruncation(\n response: string,\n finishReason: LLMFinishReason,\n): TruncationResult {\n if (finishReason === 'length') {\n const bracketInfo = countBrackets(response);\n return {\n isTruncated: true,\n reason: 'finish_reason',\n partialContent: response,\n lastCompleteElement: findLastCompleteElement(response),\n missingCloseBrackets: bracketInfo.missingCloseBrackets,\n missingCloseBraces: bracketInfo.missingCloseBraces,\n };\n }\n\n try {\n JSON.parse(response);\n return { isTruncated: false, reason: 'none' };\n } catch {\n // JSON is invalid, check if due to truncation\n }\n\n if (finishReason === 'stop' || finishReason === null) {\n const trimmed = response.trim();\n\n const isMidContent =\n trimmed.endsWith(',') ||\n trimmed.endsWith(':') ||\n trimmed.endsWith('\": ') ||\n /:\\s*$/.test(trimmed) ||\n /,\\s*$/.test(trimmed);\n\n if (isMidContent) {\n const bracketInfo = countBrackets(response);\n return {\n isTruncated: true,\n reason: 'json_incomplete',\n partialContent: response,\n lastCompleteElement: findLastCompleteElement(response),\n missingCloseBrackets: bracketInfo.missingCloseBrackets,\n missingCloseBraces: bracketInfo.missingCloseBraces,\n };\n }\n\n try {\n const closed = autoCloseJson(trimmed);\n JSON.parse(closed);\n return { isTruncated: false, reason: 'none' };\n } catch {\n return { isTruncated: false, reason: 'none' };\n }\n }\n\n const bracketInfo = countBrackets(response);\n if (\n bracketInfo.missingCloseBrackets > 0 ||\n bracketInfo.missingCloseBraces > 0\n ) {\n return {\n isTruncated: true,\n reason: 'bracket_mismatch',\n partialContent: response,\n lastCompleteElement: findLastCompleteElement(response),\n missingCloseBrackets: bracketInfo.missingCloseBrackets,\n missingCloseBraces: bracketInfo.missingCloseBraces,\n };\n }\n\n return { isTruncated: false, reason: 'none' };\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\nfunction countBrackets(json: string): {\n openBrackets: number;\n closeBrackets: number;\n openBraces: number;\n closeBraces: number;\n missingCloseBrackets: number;\n missingCloseBraces: number;\n} {\n let inString = false;\n let escaped = false;\n let openBrackets = 0;\n let closeBrackets = 0;\n let openBraces = 0;\n let closeBraces = 0;\n\n for (const char of json) {\n if (escaped) {\n escaped = false;\n continue;\n }\n if (char === '\\\\' && inString) {\n escaped = true;\n continue;\n }\n if (char === '\"') {\n inString = !inString;\n continue;\n }\n if (inString) continue;\n\n switch (char) {\n case '[':\n openBrackets++;\n break;\n case ']':\n closeBrackets++;\n break;\n case '{':\n openBraces++;\n break;\n case '}':\n closeBraces++;\n break;\n }\n }\n\n return {\n openBrackets,\n closeBrackets,\n openBraces,\n closeBraces,\n missingCloseBrackets: Math.max(0, openBrackets - closeBrackets),\n missingCloseBraces: Math.max(0, openBraces - closeBraces),\n };\n}\n\nexport function findLastCompleteElement(json: string): unknown | null {\n const autoClosed = autoCloseJson(json);\n try {\n return JSON.parse(autoClosed);\n } catch {\n // Auto-close didn't work\n }\n\n const trimmed = json.trim();\n\n if (trimmed.startsWith('[')) {\n const lastCompleteIndex = findLastCompleteArrayElement(trimmed);\n if (lastCompleteIndex > 0) {\n const subset = trimmed.substring(0, lastCompleteIndex) + ']';\n try {\n return JSON.parse(subset);\n } catch {\n // Continue\n }\n }\n }\n\n if (trimmed.startsWith('{')) {\n const closed = autoCloseJson(trimmed);\n try {\n return JSON.parse(closed);\n } catch {\n const lastCompleteIndex = findLastCompleteObjectProperty(trimmed);\n if (lastCompleteIndex > 0) {\n const subset = trimmed.substring(0, lastCompleteIndex) + '}';\n try {\n return JSON.parse(subset);\n } catch {\n // Give up\n }\n }\n }\n }\n\n return null;\n}\n\nfunction findLastCompleteArrayElement(json: string): number {\n let depth = 0;\n let inString = false;\n let escaped = false;\n let lastCompleteElementEnd = -1;\n\n for (let i = 0; i < json.length; i++) {\n const char = json[i];\n\n if (escaped) {\n escaped = false;\n continue;\n }\n if (char === '\\\\' && inString) {\n escaped = true;\n continue;\n }\n if (char === '\"') {\n inString = !inString;\n continue;\n }\n if (inString) continue;\n\n if (char === '[' || char === '{') {\n depth++;\n } else if (char === ']' || char === '}') {\n depth--;\n if (depth === 1) {\n lastCompleteElementEnd = i + 1;\n }\n } else if (char === ',' && depth === 1) {\n lastCompleteElementEnd = i;\n }\n }\n\n return lastCompleteElementEnd > 0 ? lastCompleteElementEnd : -1;\n}\n\nfunction findLastCompleteObjectProperty(json: string): number {\n let depth = 0;\n let inString = false;\n let escaped = false;\n let lastCommaIndex = -1;\n\n for (let i = 0; i < json.length; i++) {\n const char = json[i];\n\n if (escaped) {\n escaped = false;\n continue;\n }\n if (char === '\\\\' && inString) {\n escaped = true;\n continue;\n }\n if (char === '\"') {\n inString = !inString;\n continue;\n }\n if (inString) continue;\n\n if (char === '[' || char === '{') {\n depth++;\n } else if (char === ']' || char === '}') {\n depth--;\n } else if (char === ',' && depth === 1) {\n lastCommaIndex = i;\n }\n }\n\n return lastCommaIndex > 0 ? lastCommaIndex : -1;\n}\n\nexport function isLikelyTruncated(content: string): boolean {\n const trimmed = content.trim();\n if (!trimmed) return false;\n\n const brackets = countBrackets(trimmed);\n if (\n brackets.missingCloseBrackets > 0 ||\n brackets.missingCloseBraces > 0\n ) {\n return true;\n }\n\n const abruptEndings = [\n /,\\s*$/,\n /:\\s*$/,\n /\"\\s*:\\s*$/,\n /\\[\\s*$/,\n /{\\s*$/,\n ];\n\n for (const pattern of abruptEndings) {\n if (pattern.test(trimmed)) return true;\n }\n\n return false;\n}\n","/**\n * LLM Continuation Utility\n *\n * Handles truncated LLM responses with automatic continuation.\n * - Detects truncation via finish_reason and JSON structure\n * - Automatically continues with full context\n * - Merges partial and continuation responses\n * - Salvages partial data if max continuations reached\n *\n * @packageDocumentation\n */\n\nimport { z } from 'zod';\nimport { LLMClient, type LLMFinishReason } from './client.js';\nimport { detectTruncation } from './truncation-detector.js';\nimport { extractJsonFromText, autoCloseJson, isValidJson } from './json-parser.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ContinuationOptions<T> {\n client: LLMClient;\n systemPrompt: string;\n userPrompt: string;\n schema?: z.ZodSchema<T>;\n maxTokens?: number;\n maxContinuations?: number;\n maxRetries?: number;\n buildContinuationPrompt: (\n partialResponse: string,\n attempt: number,\n ) => string;\n continuationSystemPrompt?: string;\n}\n\nexport interface ContinuationResult<T> {\n data: T;\n raw: string;\n continuationCount: number;\n warnings: string[];\n wasSalvaged: boolean;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst DEFAULT_MAX_TOKENS = 8192;\nconst DEFAULT_MAX_CONTINUATIONS = 3;\n\n/**\n * Default continuation system prompt.\n * Used when no custom continuationSystemPrompt is provided.\n */\nconst DEFAULT_CONTINUATION_SYSTEM_PROMPT = `You are a JSON continuation assistant. Your ONLY job is to continue generating JSON from where the previous response was truncated.\n\nRules:\n1. Continue from EXACTLY where the previous output stopped\n2. Do NOT repeat any content already generated\n3. Complete the JSON structure properly with all closing brackets\n4. Do NOT wrap in markdown code blocks\n5. Output ONLY the continuation JSON, nothing else`;\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Merge a previous partial response with a continuation.\n *\n * Handles JSON structure merging, removing markdown code blocks,\n * and ensuring proper comma separation between merged parts.\n *\n * @param {string} previous - The previous partial response\n * @param {string} continuation - The continuation to merge\n * @returns {string} The merged response\n */\nexport function mergeResponses(\n previous: string,\n continuation: string,\n): string {\n const trimmedPrev = previous.trimEnd();\n const trimmedCont = continuation.trimStart();\n\n let cleanedCont = trimmedCont\n .replace(/^```json?\\s*/i, '')\n .replace(/```\\s*$/i, '')\n .trim();\n\n if (cleanedCont.startsWith('{')) {\n try {\n const contParsed = JSON.parse(autoCloseJson(cleanedCont));\n const keys = Object.keys(contParsed);\n if (keys.length === 1 && Array.isArray(contParsed[keys[0]])) {\n cleanedCont = contParsed[keys[0]]\n .map((item: unknown) => JSON.stringify(item))\n .join(',\\n');\n }\n } catch {\n // Continue with original cleaning\n }\n }\n\n if (cleanedCont.startsWith('}') || cleanedCont.startsWith(']')) {\n return trimmedPrev + cleanedCont;\n }\n\n const prevEndsWithValue = /[\\}\\]\\\"\\d]$/.test(trimmedPrev);\n const contStartsWithValue = /^[\\{\\[\\\"]/.test(cleanedCont);\n\n if (prevEndsWithValue && contStartsWithValue) {\n return trimmedPrev + ',\\n' + cleanedCont;\n }\n\n return trimmedPrev + cleanedCont;\n}\n\nexport function salvagePartialResponse<T>(rawResponse: string): T | null {\n console.warn('[Continuation] Attempting to salvage partial response');\n\n try {\n const cleanedResponse = extractJsonFromText(rawResponse) || rawResponse;\n const closed = autoCloseJson(cleanedResponse);\n const parsed = JSON.parse(closed) as T;\n console.log('[Continuation] Successfully salvaged partial response');\n return parsed;\n } catch (error) {\n console.error('[Continuation] Could not salvage response:', error);\n }\n\n return null;\n}\n\n// ============================================================================\n// Main Function\n// ============================================================================\n\n/**\n * Call an LLM with automatic continuation handling.\n *\n * Manages token limits by detecting truncation and making continuation\n * calls until the complete response is received or max continuations reached.\n *\n * @template T - Expected response type\n * @param {ContinuationOptions<T>} options - Continuation call options\n * @returns {Promise<ContinuationResult<T>>} Result with data and continuation metadata\n */\nexport async function callWithContinuation<T>(\n options: ContinuationOptions<T>,\n): Promise<ContinuationResult<T>> {\n const {\n client,\n systemPrompt,\n userPrompt,\n schema,\n maxTokens = DEFAULT_MAX_TOKENS,\n maxContinuations = DEFAULT_MAX_CONTINUATIONS,\n buildContinuationPrompt,\n continuationSystemPrompt = DEFAULT_CONTINUATION_SYSTEM_PROMPT,\n } = options;\n\n let rawResponse = '';\n let continuationCount = 0;\n const warnings: string[] = [];\n let wasSalvaged = false;\n\n console.log('[Continuation] Starting LLM call with continuation support');\n console.log(\n `[Continuation] Max tokens: ${maxTokens}, Max continuations: ${maxContinuations}`,\n );\n\n try {\n const response = await client.callRawWithMetadata({\n systemPrompt,\n userPrompt,\n maxTokens,\n });\n\n rawResponse = extractJsonFromText(response.raw) || response.raw;\n\n console.log(\n `[Continuation] Initial response: ${rawResponse.length} chars, finish_reason: ${response.finishReason}`,\n );\n\n let truncation = detectTruncation(rawResponse, response.finishReason);\n\n while (truncation.isTruncated && continuationCount < maxContinuations) {\n continuationCount++;\n const warningMsg = `Response truncated (${truncation.reason}), continuing (attempt ${continuationCount}/${maxContinuations})`;\n console.log(`[Continuation] ${warningMsg}`);\n warnings.push(warningMsg);\n\n const contPrompt = buildContinuationPrompt(\n rawResponse,\n continuationCount,\n );\n\n const contResponse = await client.callRawWithMetadata({\n systemPrompt: continuationSystemPrompt,\n userPrompt: contPrompt,\n maxTokens,\n });\n\n console.log(\n `[Continuation] Continuation response: ${contResponse.raw.length} chars, finish_reason: ${contResponse.finishReason}`,\n );\n\n const cleanedContResponse =\n extractJsonFromText(contResponse.raw) || contResponse.raw;\n rawResponse = mergeResponses(rawResponse, cleanedContResponse);\n\n truncation = detectTruncation(rawResponse, contResponse.finishReason);\n }\n\n if (\n continuationCount >= maxContinuations &&\n truncation.isTruncated\n ) {\n console.warn(\n `[Continuation] Reached max continuations (${maxContinuations}), attempting to salvage...`,\n );\n warnings.push(\n `Reached max continuations - some content may be incomplete`,\n );\n wasSalvaged = true;\n }\n\n const cleanedResponse =\n extractJsonFromText(rawResponse) || rawResponse;\n let data: T;\n\n try {\n if (isValidJson(cleanedResponse)) {\n data = JSON.parse(cleanedResponse) as T;\n } else {\n const closed = autoCloseJson(cleanedResponse);\n data = JSON.parse(closed) as T;\n if (!wasSalvaged) {\n warnings.push('Response required auto-closing of JSON brackets');\n }\n }\n } catch (parseError) {\n const salvaged = salvagePartialResponse<T>(cleanedResponse);\n if (salvaged) {\n data = salvaged;\n wasSalvaged = true;\n warnings.push('Response was salvaged from partial data');\n } else {\n throw new Error(\n `Failed to parse response after ${continuationCount} continuations: ${parseError}`,\n );\n }\n }\n\n if (schema) {\n try {\n data = schema.parse(data);\n } catch (validationError) {\n console.warn(\n '[Continuation] Schema validation failed:',\n validationError,\n );\n warnings.push(`Schema validation issue: ${validationError}`);\n }\n }\n\n console.log(\n `[Continuation] Complete. Continuations: ${continuationCount}, Warnings: ${warnings.length}`,\n );\n\n return {\n data,\n raw: rawResponse,\n continuationCount,\n warnings,\n wasSalvaged,\n };\n } catch (error) {\n console.error('[Continuation] Error during LLM call:', error);\n throw error;\n }\n}\n\n/**\n * Build a generic continuation prompt.\n *\n * Creates a prompt that instructs the LLM to continue from where\n * a previous truncated response left off.\n *\n * @param {string} context - Original context/prompt\n * @param {string} partialResponse - The partial response generated so far\n * @param {number} attempt - Current continuation attempt number\n * @param {number} [maxAttempts] - Maximum allowed continuation attempts\n * @returns {string} The formatted continuation prompt\n */\nexport function buildGenericContinuationPrompt(\n context: string,\n partialResponse: string,\n attempt: number,\n maxAttempts: number = DEFAULT_MAX_CONTINUATIONS,\n): string {\n return `## CONTINUATION REQUEST (Attempt ${attempt}/${maxAttempts})\n\nYour previous response was truncated. Continue generating from where you left off.\n\n### ORIGINAL CONTEXT\n${context}\n\n### WHAT YOU GENERATED SO FAR\n\\`\\`\\`json\n${partialResponse}\n\\`\\`\\`\n\n### INSTRUCTIONS\n1. Continue from EXACTLY where the response was cut off\n2. Do NOT repeat any content already generated\n3. Complete the JSON structure properly\n4. Do NOT wrap your response in markdown code blocks\n\nContinue generating now:`;\n}\n"],"mappings":"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","names":["bracketInfo"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/llm",
3
- "version": "2.41.0",
3
+ "version": "2.43.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",
@@ -46,11 +46,11 @@
46
46
  "@almadar/logger": "^1.11.0"
47
47
  },
48
48
  "peerDependencies": {
49
- "@almadar/core": "^10.66.0"
49
+ "@almadar/core": "^10.71.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@almadar/core": "^10.66.0",
53
- "@almadar/eslint-plugin": "^2.15.0",
52
+ "@almadar/core": "^10.71.0",
53
+ "@almadar/eslint-plugin": "^2.16.0",
54
54
  "@types/node": "^22.0.0",
55
55
  "@typescript-eslint/parser": "8.65.0",
56
56
  "eslint": "10.0.0",
package/src/client.ts CHANGED
@@ -132,7 +132,7 @@ type ChatModel = ChatOpenAI | ChatAnthropic;
132
132
  // Types
133
133
  // ============================================================================
134
134
 
135
- export type LLMProvider = 'openai' | 'deepseek' | 'anthropic' | 'kimi' | 'openrouter' | 'orbgen' | 'masar';
135
+ export type LLMProvider = 'openai' | 'deepseek' | 'anthropic' | 'kimi' | 'openrouter' | 'orbgen' | 'masar' | 'lmstudio';
136
136
 
137
137
  export interface ProviderConfig {
138
138
  apiKey: string;
@@ -387,6 +387,16 @@ const PROVIDER_CONFIGS: Record<LLMProvider, () => ProviderConfig> = {
387
387
  defaultModel: process.env.MASAR_SUBAGENT_MODEL ?? 'subagent',
388
388
  };
389
389
  },
390
+ lmstudio: () => {
391
+ // Local LM Studio server (OpenAI-compatible), running as a background
392
+ // service — no env var required, unlike orbgen/masar's remote endpoints.
393
+ const baseUrl = process.env.LMSTUDIO_URL ?? 'http://127.0.0.1:1234/v1';
394
+ return {
395
+ apiKey: 'not-needed',
396
+ baseUrl,
397
+ defaultModel: process.env.LMSTUDIO_MODEL ?? 'qwen3-30b-a3b',
398
+ };
399
+ },
390
400
  };
391
401
 
392
402
  export const DEEPSEEK_MODELS = {
@@ -451,6 +461,34 @@ export const OPENROUTER_MODELS = {
451
461
  GLM_4_7: 'z-ai/glm-4.7',
452
462
  } as const;
453
463
 
464
+ export interface ProviderCatalogEntry {
465
+ provider: LLMProvider;
466
+ /** Enumerable models for this provider; empty for env/URL-driven
467
+ * providers (orbgen, masar, lmstudio) with no closed set to choose from. */
468
+ models: string[];
469
+ defaultModel: string;
470
+ }
471
+
472
+ /** Single source of truth for "what providers/models does this package
473
+ * support" — consumers (setup wizards, model pickers) should read this
474
+ * instead of importing individual `*_MODELS` consts and re-deriving the
475
+ * provider list themselves. */
476
+ export const PROVIDER_CATALOG: ProviderCatalogEntry[] = [
477
+ { provider: 'anthropic', models: Object.values(ANTHROPIC_MODELS), defaultModel: ANTHROPIC_MODELS.CLAUDE_SONNET_4_5 },
478
+ { provider: 'openai', models: Object.values(OPENAI_MODELS), defaultModel: OPENAI_MODELS.GPT_5_1 },
479
+ { provider: 'deepseek', models: Object.values(DEEPSEEK_MODELS), defaultModel: DEEPSEEK_MODELS.CHAT },
480
+ { provider: 'kimi', models: Object.values(KIMI_MODELS), defaultModel: KIMI_MODELS.K2_5 },
481
+ { provider: 'openrouter', models: Object.values(OPENROUTER_MODELS), defaultModel: OPENROUTER_MODELS.QWEN_3_235B },
482
+ { provider: 'orbgen', models: [], defaultModel: 'orbgen-v2' },
483
+ { provider: 'masar', models: [], defaultModel: 'subagent' },
484
+ { provider: 'lmstudio', models: [], defaultModel: 'qwen3-30b-a3b' },
485
+ ];
486
+
487
+ /** Providers with an enumerable model catalog — safe to offer in a picker. */
488
+ export function listSelectableProviders(): ProviderCatalogEntry[] {
489
+ return PROVIDER_CATALOG.filter((p) => p.models.length > 0);
490
+ }
491
+
454
492
  const DEFAULT_TEMPERATURE = 0.3;
455
493
 
456
494
  // ============================================================================
@@ -488,7 +526,8 @@ export class LLMClient {
488
526
  this.providerConfig = { ...this.providerConfig, baseUrl: defaultBaseUrl };
489
527
  }
490
528
  this.modelName = options.model || this.providerConfig.defaultModel;
491
- this.rawTimeoutMs = options.rawTimeoutMs ?? LLMClient.DEFAULT_RAW_TIMEOUT_MS;
529
+ this.rawTimeoutMs = options.rawTimeoutMs ??
530
+ (this.provider === 'lmstudio' ? LLMClient.LOCAL_PROVIDER_RAW_TIMEOUT_MS : LLMClient.DEFAULT_RAW_TIMEOUT_MS);
492
531
  this.reasoningEffort = options.reasoningEffort;
493
532
 
494
533
  const keyPreview = this.providerConfig.apiKey.slice(-4);
@@ -863,6 +902,17 @@ export class LLMClient {
863
902
  */
864
903
  private static readonly DEFAULT_RAW_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
865
904
 
905
+ /**
906
+ * Local/self-hosted providers (e.g. a quantized model on consumer
907
+ * hardware) routinely need far longer than 5 min for a single
908
+ * tool-calling turn under heavy reasoning + a large tool schema — the
909
+ * 5 min default was observed clipping every attempt at exactly
910
+ * 300000ms with no completion in sight, not failing on genuinely slow
911
+ * output. Give them more room; still finite so a truly wedged call
912
+ * doesn't hang forever.
913
+ */
914
+ private static readonly LOCAL_PROVIDER_RAW_TIMEOUT_MS = 20 * 60 * 1000; // 20 min
915
+
866
916
  /**
867
917
  * Wrap a `model.invoke(...)` with start/finish/error logging and a hard
868
918
  * abort-driven timeout. Returns whatever the underlying `invoke` returns.
@@ -1605,6 +1655,8 @@ export function isProviderAvailable(provider: LLMProvider): boolean {
1605
1655
  return !!process.env.ORBGEN_URL;
1606
1656
  case 'masar':
1607
1657
  return !!process.env.MASAR_SUBAGENT_URL;
1658
+ case 'lmstudio':
1659
+ return true; // local background service with a sane default URL, no env var required
1608
1660
  default:
1609
1661
  return false;
1610
1662
  }
package/src/index.ts CHANGED
@@ -28,6 +28,9 @@ export {
28
28
  ANTHROPIC_MODELS,
29
29
  KIMI_MODELS,
30
30
  OPENROUTER_MODELS,
31
+ PROVIDER_CATALOG,
32
+ listSelectableProviders,
33
+ type ProviderCatalogEntry,
31
34
  type LLMProvider,
32
35
  type ProviderConfig,
33
36
  type LLMClientOptions,
@@ -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';\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 /** DeepSeek V4: controls reasoning effort (\"none\" disables thinking tokens). */\n reasoning_effort?: string;\n /** OpenRouter: `{ enabled: false }` stops the router burning tokens on CoT. */\n reasoning?: { enabled: 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 * Reasoning effort for thinking models (e.g. DeepSeek V4).\n * Set to \"none\" to disable reasoning tokens for faster content generation.\n */\n reasoningEffort?: string;\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 /** Prompt tokens served from the provider's prefix cache (OpenAI-style\n * `prompt_tokens_details.cached_tokens`, deepseek-native\n * `prompt_cache_hit_tokens`). Absent when the provider reports neither. */\n cachedPromptTokens?: 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// Vision (Anthropic image content-parts)\n// ============================================================================\n\n/** Media types the Anthropic base64 image source accepts. */\nexport type VisionImageMediaType = Anthropic.Base64ImageSource['media_type'];\n\n/** A single image sent as a base64 content-part (no `data:` URI prefix). */\nexport interface VisionImagePart {\n base64: string;\n mediaType: VisionImageMediaType;\n}\n\nexport interface VisionCallOptions<T = string> {\n systemPrompt?: string;\n userText: string;\n images: ReadonlyArray<VisionImagePart>;\n /** Zod schema for structured JSON output — reuses the package's `parseJsonResponse` mechanism. */\n schema?: z.ZodSchema<T>;\n maxTokens?: number;\n temperature?: number;\n skipSchemaValidation?: boolean;\n signal?: AbortSignal;\n}\n\n/**\n * Build the Anthropic user-message content array for a vision call: one text\n * block followed by one image block per screenshot, grounded entirely in the\n * SDK's own typed shapes. Pure and side-effect-free so it can be unit-tested\n * without any API call.\n */\nexport function buildVisionMessageContent(\n userText: string,\n images: ReadonlyArray<VisionImagePart>,\n): Anthropic.ContentBlockParam[] {\n const textBlock: Anthropic.TextBlockParam = { type: 'text', text: userText };\n const imageBlocks: Anthropic.ImageBlockParam[] = images.map((img) => ({\n type: 'image',\n source: { type: 'base64', media_type: img.mediaType, data: img.base64 },\n }));\n return [textBlock, ...imageBlocks];\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_HAIKU_4_5: 'claude-haiku-4-5',\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 // Gemma 4 26B A4B (MoE, 4B active) — matrix candidate vs deepseek default (G-MODEL-1)\n GEMMA_4_26B_A4B: 'google/gemma-4-26b-a4b-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 private reasoningEffort: string | undefined;\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 this.reasoningEffort = options.reasoningEffort;\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 streaming?: boolean;\n }): ChatModel {\n const maxTokens = options?.maxTokens;\n const temperature = options?.temperature ?? this.temperature;\n const streaming = options?.streaming ?? this.streaming;\n\n if (this.provider === 'anthropic') {\n return new ChatAnthropic({\n apiKey: this.providerConfig.apiKey,\n model: this.modelName,\n temperature,\n 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 // DeepSeek V4: disable reasoning when reasoningEffort is set\n if (this.reasoningEffort) {\n modelKwargs.reasoning_effort = this.reasoningEffort;\n }\n // OpenRouter: explicit tool_choice so the model doesn't ignore tool definitions.\n // When reasoning is disabled (reasoningEffort === 'none'), send\n // reasoning: { enabled: false } so OpenRouter doesn't burn tokens on CoT.\n if (this.provider === 'openrouter') {\n modelKwargs.tool_choice = 'auto';\n modelKwargs.usage = { include: true };\n if (this.reasoningEffort === 'none') {\n modelKwargs.reasoning = { enabled: false };\n }\n }\n\n return new ChatOpenAI({\n apiKey: this.providerConfig.apiKey,\n model: this.modelName,\n temperature: useCompletionTokens ? undefined : effectiveTemp,\n 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 streaming?: boolean;\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 // This raw-fetch path bypasses createModel()'s modelKwargs, so the\n // reasoning kill-switch must be applied here as well — without it\n // DeepSeek V4 burns the whole completion cap on reasoning_content\n // before emitting a tool call (rabit's runaway-reasoning class).\n if (this.reasoningEffort) {\n body['reasoning_effort'] = this.reasoningEffort;\n }\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'] = this.reasoningEffort === 'none' ? { enabled: false } : { 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 const rawUsage = parsed.usage as {\n prompt_tokens_details?: { cached_tokens?: number };\n prompt_cache_hit_tokens?: number;\n cost?: number;\n };\n const cachedTokens =\n rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens;\n usage = {\n promptTokens: parsed.usage.prompt_tokens,\n completionTokens: parsed.usage.completion_tokens,\n totalTokens: parsed.usage.total_tokens,\n ...(cachedTokens !== undefined ? { cachedPromptTokens: cachedTokens } : {}),\n };\n if (this.tokenTracker) {\n this.tokenTracker.addUsage(usage.promptTokens, usage.completionTokens, {\n provider: this.provider,\n cachedPromptTokens: cachedTokens ?? 0,\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 // streamRaw must always stream: force streaming on the model regardless of the client's\n // default `streaming` flag, or LangChain's .stream() degrades to a single whole-response chunk.\n const modelToUse = this.getModelWithOptions({ maxTokens, temperature, streaming: true });\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 /**\n * Send one or more images (base64 content-parts) plus a text prompt to a\n * vision-capable Anthropic model and return the response. When a `schema`\n * is supplied the text output is parsed and validated through the package's\n * `parseJsonResponse` mechanism; otherwise the raw text is returned as `T`.\n *\n * Anthropic-only — image content-parts use the native messages API. Other\n * providers throw (mirrors `callWithTools`).\n */\n async callWithVision<T = string>(\n options: VisionCallOptions<T>,\n ): Promise<LLMResponse<T>> {\n if (this.provider !== 'anthropic') {\n throw new Error(\n `LLMClient.callWithVision: vision is only supported on the anthropic provider (got ${this.provider})`,\n );\n }\n\n const {\n systemPrompt,\n userText,\n images,\n schema,\n maxTokens,\n temperature,\n skipSchemaValidation = false,\n signal,\n } = options;\n\n if (images.length === 0) {\n throw new Error('LLMClient.callWithVision: at least one image is required');\n }\n\n return this.rateLimiter.execute(async () => {\n const anthropic = new Anthropic({ apiKey: this.providerConfig.apiKey });\n const content = buildVisionMessageContent(userText, images);\n const messages: Anthropic.MessageParam[] = [{ role: 'user', content }];\n\n log.info(\n `[LLMClient:callWithVision] Invoking ${this.provider}/${this.modelName} (images=${images.length})`,\n );\n\n const response = await anthropic.messages.create(\n {\n model: this.modelName,\n max_tokens: maxTokens ?? 4096,\n temperature: temperature ?? 0,\n ...(systemPrompt ? { system: systemPrompt } : {}),\n messages,\n },\n signal ? { signal } : {},\n );\n\n const textContent = response.content.find((c) => c.type === 'text');\n const rawText =\n textContent && 'text' in textContent ? textContent.text : '';\n\n const apiUsage = response.usage;\n const usage: LLMUsage = {\n promptTokens: apiUsage.input_tokens,\n completionTokens: apiUsage.output_tokens,\n totalTokens: apiUsage.input_tokens + apiUsage.output_tokens,\n };\n if (this.tokenTracker) {\n this.tokenTracker.addUsage(usage.promptTokens, usage.completionTokens, {\n provider: this.provider,\n });\n }\n\n const finishReason: LLMFinishReason =\n response.stop_reason === 'end_turn'\n ? 'stop'\n : response.stop_reason === 'max_tokens'\n ? 'length'\n : response.stop_reason === 'tool_use'\n ? 'tool_calls'\n : null;\n\n const data: T = skipSchemaValidation\n ? parseJsonResponse<T>(rawText, undefined)\n : schema\n ? parseJsonResponse<T>(rawText, schema)\n : asGeneric<T>(rawText);\n\n return { data, raw: rawText, finishReason, usage };\n });\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":[]}