@almadar/llm 2.49.0 → 2.51.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.
package/dist/index.d.ts CHANGED
@@ -104,15 +104,17 @@ declare function cosineSimilarity(a: ReadonlyArray<number>, b: ReadonlyArray<num
104
104
  /**
105
105
  * Image Client
106
106
  *
107
- * Single-purpose client for OpenRouter's image-generation API
108
- * (`POST {baseUrl}/images`). Raw `fetch`, no SDK — mirrors
107
+ * Single-purpose client for image-generation APIs. `openrouter` posts to
108
+ * `POST {baseUrl}/images`; `local` posts an OpenAI-shaped body to
109
+ * `POST {baseUrl}/images/generations` (the MFLUX shim in
110
+ * tools/asset-workflow/python/image_server). Raw `fetch`, no SDK — mirrors
109
111
  * `embedding-client.ts`'s shape (provider tables, retry policy, the
110
112
  * three response-error surfaces).
111
113
  *
112
114
  * @packageDocumentation
113
115
  */
114
116
 
115
- type ImageProvider = 'openrouter';
117
+ type ImageProvider = 'openrouter' | 'local';
116
118
  /** Canonical image-provider resolution, mirrors `resolveDefaultEmbeddingProvider`. */
117
119
  declare function resolveDefaultImageProvider(): ImageProvider;
118
120
  declare const OPENROUTER_IMAGE_MODELS: {
@@ -126,6 +128,18 @@ declare const OPENROUTER_IMAGE_DEFAULTS: {
126
128
  readonly sheet: "bytedance-seed/seedream-5-0-lite";
127
129
  readonly preview: "google/gemini-3.1-flash-lite-image";
128
130
  };
131
+ declare const LOCAL_IMAGE_MODELS: {
132
+ readonly Z_IMAGE_TURBO: "z-image-turbo";
133
+ readonly FLUX_SCHNELL: "flux-schnell";
134
+ readonly FLUX_DEV: "flux-dev";
135
+ readonly FLUX_KONTEXT: "flux-kontext";
136
+ readonly FLUX2_KLEIN: "flux2-klein";
137
+ };
138
+ declare const LOCAL_IMAGE_DEFAULTS: {
139
+ /** Z-Image-Turbo is the default: public on Hugging Face, no token required (FLUX is gated). */
140
+ sheet: string;
141
+ preview: string;
142
+ };
129
143
  interface ImageClientOptions {
130
144
  provider?: ImageProvider;
131
145
  model?: string;
@@ -147,6 +161,10 @@ interface ImageGenerateOptions {
147
161
  quality?: 'auto' | 'low' | 'medium' | 'high';
148
162
  background?: 'auto' | 'transparent' | 'opaque';
149
163
  seed?: number;
164
+ /** Diffusion step count (local shim; 1–100). */
165
+ steps?: number;
166
+ /** img2img denoise strength 0–1 (local shim `image_strength`), used with `references`. */
167
+ strength?: number;
150
168
  references?: ReadonlyArray<ImageReference>;
151
169
  provider?: {
152
170
  order?: string[];
@@ -196,8 +214,25 @@ declare class ImageClient {
196
214
  */
197
215
  generateChecked(opts: ImageGenerateOptions): Promise<ImageGenerateResult>;
198
216
  private performGenerate;
199
- /** Retries on network failure or a 5xx status; any other response is returned as-is. */
217
+ /**
218
+ * OpenAI-compatible local server (the MFLUX shim in tools/asset-workflow).
219
+ * OpenRouter-only fields are reported in `dropped`, never sent. References
220
+ * ARE sent (as `input_references`) — the shim forwards them to mflux's
221
+ * `image_path`/`image_strength` img2img conditioning.
222
+ */
223
+ private performLocalGenerate;
224
+ private authHeaders;
225
+ private sendGenerate;
226
+ /** Retries on network failure or a 5xx status; any other response is returned as-is.
227
+ * The local provider never retries: a failed local generation (e.g. MLX OOM) cannot
228
+ * succeed on an immediate retry, and hammering a memory-pressured machine is harmful. */
200
229
  private fetchWithRetry;
230
+ /** Local generations can exceed Node's built-in 300s headersTimeout (a single
231
+ * MFLUX image routinely takes minutes), so local calls ride npm-undici's own
232
+ * fetch with the transport timers disabled — mirrors `LLMClient.localDispatcher`. */
233
+ private static localDispatcher;
234
+ private static getLocalDispatcher;
235
+ private doFetch;
201
236
  }
202
237
  declare const IMAGE_PROVIDER_CATALOG: ReadonlyArray<{
203
238
  provider: ImageProvider;
@@ -441,4 +476,4 @@ type LLMServiceActions = {
441
476
  */
442
477
  type LLMServiceContract = ServiceContract<LLMServiceActions>;
443
478
 
444
- export { type ContinuationOptions, type ContinuationResult, type EmbeddingBatchResult, EmbeddingClient, type EmbeddingClientOptions, type EmbeddingProvider, type EmbeddingResult, type EmbeddingUsage, type GeneratedImage, IMAGE_PROVIDER_CATALOG, ImageClient, type ImageClientOptions, type ImageGenerateOptions, type ImageGenerateResult, type ImageModelCapabilities, type ImageProvider, type ImageReference, LLMClient, LLMFinishReason, type LLMServiceActions, type LLMServiceContract, OPENROUTER_IMAGE_DEFAULTS, OPENROUTER_IMAGE_MODELS, type TruncationReason, type TruncationResult, buildGenericContinuationPrompt, callWithContinuation, cosineSimilarity, detectTruncation, findLastCompleteElement, isLikelyTruncated, mergeResponses, resolveDefaultEmbeddingProvider, resolveDefaultImageProvider, salvagePartialResponse };
479
+ export { type ContinuationOptions, type ContinuationResult, type EmbeddingBatchResult, EmbeddingClient, type EmbeddingClientOptions, type EmbeddingProvider, type EmbeddingResult, type EmbeddingUsage, type GeneratedImage, IMAGE_PROVIDER_CATALOG, ImageClient, type ImageClientOptions, type ImageGenerateOptions, type ImageGenerateResult, type ImageModelCapabilities, type ImageProvider, type ImageReference, LLMClient, LLMFinishReason, type LLMServiceActions, type LLMServiceContract, LOCAL_IMAGE_DEFAULTS, LOCAL_IMAGE_MODELS, OPENROUTER_IMAGE_DEFAULTS, OPENROUTER_IMAGE_MODELS, type TruncationReason, type TruncationResult, buildGenericContinuationPrompt, callWithContinuation, cosineSimilarity, detectTruncation, findLastCompleteElement, isLikelyTruncated, mergeResponses, resolveDefaultEmbeddingProvider, resolveDefaultImageProvider, salvagePartialResponse };
package/dist/index.js CHANGED
@@ -199,7 +199,8 @@ function cosineSimilarity(a, b) {
199
199
  }
200
200
 
201
201
  // src/image-client.ts
202
- var IMAGE_PROVIDERS = ["openrouter"];
202
+ import { Agent as UndiciAgent, fetch as undiciFetch } from "undici";
203
+ var IMAGE_PROVIDERS = ["openrouter", "local"];
203
204
  function resolveDefaultImageProvider() {
204
205
  const v = process.env.IMAGE_PROVIDER;
205
206
  return v && IMAGE_PROVIDERS.includes(v) ? v : "openrouter";
@@ -215,20 +216,37 @@ var OPENROUTER_IMAGE_DEFAULTS = {
215
216
  sheet: OPENROUTER_IMAGE_MODELS.SEEDREAM_5_LITE,
216
217
  preview: OPENROUTER_IMAGE_MODELS.GEMINI_3_1_FLASH_LITE_IMAGE
217
218
  };
219
+ var LOCAL_IMAGE_MODELS = {
220
+ Z_IMAGE_TURBO: "z-image-turbo",
221
+ FLUX_SCHNELL: "flux-schnell",
222
+ FLUX_DEV: "flux-dev",
223
+ FLUX_KONTEXT: "flux-kontext",
224
+ FLUX2_KLEIN: "flux2-klein"
225
+ };
226
+ var LOCAL_IMAGE_DEFAULTS = {
227
+ /** Z-Image-Turbo is the default: public on Hugging Face, no token required (FLUX is gated). */
228
+ sheet: process.env.LOCAL_IMAGE_SHEET_MODEL ?? LOCAL_IMAGE_MODELS.Z_IMAGE_TURBO,
229
+ preview: process.env.LOCAL_IMAGE_PREVIEW_MODEL ?? LOCAL_IMAGE_MODELS.Z_IMAGE_TURBO
230
+ };
218
231
  var IMAGE_MAX_ATTEMPTS = 3;
219
232
  var IMAGE_RETRY_BACKOFF_MS = [500, 2e3];
220
233
  var DEFAULT_MODELS2 = {
221
- openrouter: OPENROUTER_IMAGE_DEFAULTS.sheet
234
+ openrouter: OPENROUTER_IMAGE_DEFAULTS.sheet,
235
+ local: LOCAL_IMAGE_DEFAULTS.sheet
222
236
  };
223
237
  var DEFAULT_BASE_URLS2 = {
224
- openrouter: "https://openrouter.ai/api/v1"
238
+ openrouter: "https://openrouter.ai/api/v1",
239
+ local: "http://127.0.0.1:8180/v1"
225
240
  };
226
241
  var API_KEY_ENV_VARS2 = {
227
- openrouter: "OPEN_ROUTER_API_KEY"
242
+ openrouter: "OPEN_ROUTER_API_KEY",
243
+ local: "LOCAL_IMAGE_API_KEY"
228
244
  };
229
245
  var IMAGE_CHECKED_PARAM_NAMES = {
230
246
  background: "background",
231
247
  seed: "seed",
248
+ steps: "steps",
249
+ strength: "image_strength",
232
250
  references: "input_references"
233
251
  };
234
252
  function isJsonObject(value) {
@@ -272,15 +290,15 @@ function collectSupportedParameters(body) {
272
290
  }
273
291
  return params;
274
292
  }
275
- var ImageClient = class {
293
+ var _ImageClient = class _ImageClient {
276
294
  constructor(opts = {}) {
277
295
  this.capabilitiesCache = /* @__PURE__ */ new Map();
278
- this.provider = opts.provider ?? "openrouter";
296
+ this.provider = opts.provider ?? resolveDefaultImageProvider();
279
297
  this.model = opts.model ?? DEFAULT_MODELS2[this.provider];
280
298
  this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URLS2[this.provider]).replace(/\/$/, "");
281
299
  this.fetchImpl = opts.fetchImpl ?? fetch;
282
- const apiKey = opts.apiKey ?? process.env[API_KEY_ENV_VARS2[this.provider]];
283
- if (!apiKey) {
300
+ const apiKey = opts.apiKey ?? process.env[API_KEY_ENV_VARS2[this.provider]] ?? "";
301
+ if (!apiKey && this.provider !== "local") {
284
302
  throw new Error(
285
303
  `ImageClient: API key for provider '${this.provider}' not found. Set ${API_KEY_ENV_VARS2[this.provider]} in your environment or pass options.apiKey.`
286
304
  );
@@ -300,9 +318,18 @@ var ImageClient = class {
300
318
  const model = modelId ?? this.model;
301
319
  const cached = this.capabilitiesCache.get(model);
302
320
  if (cached) return cached;
321
+ if (this.provider === "local") {
322
+ const local = {
323
+ model,
324
+ supportedParameters: /* @__PURE__ */ new Set(["seed", IMAGE_CHECKED_PARAM_NAMES.steps, IMAGE_CHECKED_PARAM_NAMES.strength, IMAGE_CHECKED_PARAM_NAMES.references]),
325
+ raw: {}
326
+ };
327
+ this.capabilitiesCache.set(model, local);
328
+ return local;
329
+ }
303
330
  const response = await this.fetchWithRetry(`${this.baseUrl}/images/models/${model}/endpoints`, {
304
331
  method: "GET",
305
- headers: { Authorization: `Bearer ${this.apiKey}` }
332
+ headers: this.authHeaders()
306
333
  });
307
334
  if (!response.ok) {
308
335
  const errorText = await response.text().catch(() => "<no body>");
@@ -334,6 +361,14 @@ var ImageClient = class {
334
361
  delete filtered.seed;
335
362
  dropped.push(IMAGE_CHECKED_PARAM_NAMES.seed);
336
363
  }
364
+ if (filtered.steps !== void 0 && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.steps)) {
365
+ delete filtered.steps;
366
+ dropped.push(IMAGE_CHECKED_PARAM_NAMES.steps);
367
+ }
368
+ if (filtered.strength !== void 0 && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.strength)) {
369
+ delete filtered.strength;
370
+ dropped.push(IMAGE_CHECKED_PARAM_NAMES.strength);
371
+ }
337
372
  if (filtered.references !== void 0 && filtered.references.length > 0 && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.references)) {
338
373
  delete filtered.references;
339
374
  dropped.push(IMAGE_CHECKED_PARAM_NAMES.references);
@@ -345,6 +380,9 @@ var ImageClient = class {
345
380
  throw new Error("ImageClient.generate: `size` cannot be combined with `resolution`/`aspectRatio` \u2014 pass one or the other");
346
381
  }
347
382
  const model = opts.model ?? this.model;
383
+ if (this.provider === "local") {
384
+ return this.performLocalGenerate(opts, model, dropped);
385
+ }
348
386
  const requestBody = { model, prompt: opts.prompt, output_format: "png" };
349
387
  if (opts.n !== void 0) requestBody.n = opts.n;
350
388
  if (opts.size !== void 0) requestBody.size = opts.size;
@@ -353,6 +391,7 @@ var ImageClient = class {
353
391
  if (opts.quality !== void 0) requestBody.quality = opts.quality;
354
392
  if (opts.background !== void 0) requestBody.background = opts.background;
355
393
  if (opts.seed !== void 0) requestBody.seed = opts.seed;
394
+ if (opts.steps !== void 0) requestBody.steps = opts.steps;
356
395
  if (opts.references !== void 0 && opts.references.length > 0) {
357
396
  requestBody.input_references = opts.references.map((r) => ({ type: "image_url", image_url: { url: r.url } }));
358
397
  }
@@ -363,9 +402,37 @@ var ImageClient = class {
363
402
  if (opts.provider.allowFallbacks !== void 0) providerField.allow_fallbacks = opts.provider.allowFallbacks;
364
403
  requestBody.provider = providerField;
365
404
  }
366
- const response = await this.fetchWithRetry(`${this.baseUrl}/images`, {
405
+ return this.sendGenerate(`${this.baseUrl}/images`, requestBody, model, dropped);
406
+ }
407
+ /**
408
+ * OpenAI-compatible local server (the MFLUX shim in tools/asset-workflow).
409
+ * OpenRouter-only fields are reported in `dropped`, never sent. References
410
+ * ARE sent (as `input_references`) — the shim forwards them to mflux's
411
+ * `image_path`/`image_strength` img2img conditioning.
412
+ */
413
+ async performLocalGenerate(opts, model, dropped) {
414
+ const requestBody = { model, prompt: opts.prompt };
415
+ if (opts.n !== void 0) requestBody.n = opts.n;
416
+ if (opts.size !== void 0) requestBody.size = opts.size;
417
+ if (opts.seed !== void 0) requestBody.seed = opts.seed;
418
+ if (opts.steps !== void 0) requestBody.steps = opts.steps;
419
+ if (opts.strength !== void 0) requestBody.image_strength = opts.strength;
420
+ if (opts.references !== void 0 && opts.references.length > 0) {
421
+ requestBody.input_references = opts.references.map((r) => ({ type: "image_url", image_url: { url: r.url } }));
422
+ }
423
+ for (const field of ["resolution", "aspectRatio", "quality", "background"]) {
424
+ if (opts[field] !== void 0) dropped.push(field);
425
+ }
426
+ if (opts.provider !== void 0) dropped.push("provider");
427
+ return this.sendGenerate(`${this.baseUrl}/images/generations`, requestBody, model, dropped);
428
+ }
429
+ authHeaders() {
430
+ return this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {};
431
+ }
432
+ async sendGenerate(url, requestBody, model, dropped) {
433
+ const response = await this.fetchWithRetry(url, {
367
434
  method: "POST",
368
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` },
435
+ headers: { "Content-Type": "application/json", ...this.authHeaders() },
369
436
  body: JSON.stringify(requestBody)
370
437
  });
371
438
  if (!response.ok) {
@@ -403,31 +470,62 @@ var ImageClient = class {
403
470
  };
404
471
  return { images, usage, model, dropped };
405
472
  }
406
- /** Retries on network failure or a 5xx status; any other response is returned as-is. */
473
+ /** Retries on network failure or a 5xx status; any other response is returned as-is.
474
+ * The local provider never retries: a failed local generation (e.g. MLX OOM) cannot
475
+ * succeed on an immediate retry, and hammering a memory-pressured machine is harmful. */
407
476
  async fetchWithRetry(url, init) {
477
+ const maxAttempts = this.provider === "local" ? 1 : IMAGE_MAX_ATTEMPTS;
408
478
  let lastNetworkError;
409
479
  let response;
410
- for (let attempt = 1; attempt <= IMAGE_MAX_ATTEMPTS; attempt++) {
480
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
411
481
  try {
412
- response = await this.fetchImpl(url, init);
482
+ response = await this.doFetch(url, init);
413
483
  } catch (e) {
414
484
  response = void 0;
415
485
  lastNetworkError = e instanceof Error ? e : new Error(String(e));
416
486
  }
417
487
  if (response !== void 0 && response.status < 500) return response;
418
488
  const backoff = IMAGE_RETRY_BACKOFF_MS[attempt - 1];
419
- if (backoff === void 0) break;
489
+ if (backoff === void 0 || attempt >= maxAttempts) break;
420
490
  await new Promise((r) => setTimeout(r, backoff));
421
491
  }
422
492
  if (response) return response;
423
- throw lastNetworkError ?? new Error(`ImageClient: ${this.provider} request failed after ${IMAGE_MAX_ATTEMPTS} attempts`);
493
+ throw lastNetworkError ?? new Error(`ImageClient: ${this.provider} request failed after ${maxAttempts} attempts`);
494
+ }
495
+ static getLocalDispatcher() {
496
+ if (_ImageClient.localDispatcher === null) {
497
+ _ImageClient.localDispatcher = new UndiciAgent({ headersTimeout: 0, bodyTimeout: 0 });
498
+ }
499
+ return _ImageClient.localDispatcher;
500
+ }
501
+ async doFetch(url, init) {
502
+ if (this.provider === "local" && this.fetchImpl === fetch) {
503
+ const uinit = {
504
+ method: init.method,
505
+ headers: init.headers,
506
+ body: init.body,
507
+ dispatcher: _ImageClient.getLocalDispatcher()
508
+ };
509
+ return undiciFetch(url, uinit);
510
+ }
511
+ return this.fetchImpl(url, init);
424
512
  }
425
513
  };
514
+ /** Local generations can exceed Node's built-in 300s headersTimeout (a single
515
+ * MFLUX image routinely takes minutes), so local calls ride npm-undici's own
516
+ * fetch with the transport timers disabled — mirrors `LLMClient.localDispatcher`. */
517
+ _ImageClient.localDispatcher = null;
518
+ var ImageClient = _ImageClient;
426
519
  var IMAGE_PROVIDER_CATALOG = [
427
520
  {
428
521
  provider: "openrouter",
429
522
  models: Object.values(OPENROUTER_IMAGE_MODELS),
430
523
  defaultModel: OPENROUTER_IMAGE_DEFAULTS.sheet
524
+ },
525
+ {
526
+ provider: "local",
527
+ models: Object.values(LOCAL_IMAGE_MODELS),
528
+ defaultModel: LOCAL_IMAGE_DEFAULTS.sheet
431
529
  }
432
530
  ];
433
531
 
@@ -833,6 +931,8 @@ export {
833
931
  ImageClient,
834
932
  KIMI_MODELS,
835
933
  LLMClient,
934
+ LOCAL_IMAGE_DEFAULTS,
935
+ LOCAL_IMAGE_MODELS,
836
936
  MasarError,
837
937
  MasarProvider,
838
938
  OPENAI_MODELS,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/embedding-client.ts","../src/image-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 * - `lmstudio` (default model `text-embedding-bge-base-en-v1.5`, same\n * bge-base-en-v1.5 weights as the openrouter default, run locally) —\n * local LM Studio server, no API key. Mirrors `client.ts`'s\n * `PROVIDER_CONFIGS.lmstudio`: base URL from `LMSTUDIO_URL` (default\n * `http://127.0.0.1:1234/v1`), model from `LMSTUDIO_EMBEDDING_MODEL`.\n *\n * All three 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' | 'lmstudio';\n\nconst EMBEDDING_PROVIDERS: readonly EmbeddingProvider[] = ['openai', 'openrouter', 'lmstudio'];\n\n/**\n * Canonical embedding-provider resolution — the ONE place every downstream\n * consumer should defer to instead of hardcoding a literal provider.\n * Mirrors the `EMBEDDING_PROVIDER` env var `@almadar-io/integrations`'s\n * `RuntimeIntegrationManager`/`getEmbeddings()` already reads (its own\n * `?? 'openrouter'` fallback is the precedent this centralizes).\n *\n * Defaults to `openrouter` — UNCHANGED from every existing deployment's\n * behavior — so nothing (cloud/server included) switches to a local-only\n * provider unless an operator explicitly sets `EMBEDDING_PROVIDER=lmstudio`\n * in that specific environment (e.g. a dev machine running LM Studio).\n * Consumers: `@almadar-io/rabit`'s catalog narrower\n * (`runtime/services/embedding.ts`) and `@almadar/workspace`'s index\n * embedder (`workspace-index/embedder.ts`).\n */\nexport function resolveDefaultEmbeddingProvider(): EmbeddingProvider {\n const v = process.env.EMBEDDING_PROVIDER;\n return v && (EMBEDDING_PROVIDERS as readonly string[]).includes(v) ? (v as EmbeddingProvider) : 'openrouter';\n}\n\nexport interface EmbeddingClientOptions {\n provider: EmbeddingProvider;\n /** Defaults: openai → text-embedding-3-small, openrouter → baai/bge-base-en-v1.5, lmstudio → text-embedding-bge-base-en-v1.5. */\n model?: string;\n /** Override API key. Defaults to provider-specific env var; lmstudio needs none. */\n apiKey?: string;\n /** Override base URL. Defaults to provider canonical endpoint (lmstudio: `LMSTUDIO_URL`). */\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 lmstudio: 'text-embedding-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 lmstudio: 'http://127.0.0.1:1234/v1',\n};\n\nconst API_KEY_ENV_VARS: Partial<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 ?? (this.provider === 'lmstudio' ? process.env.LMSTUDIO_EMBEDDING_MODEL ?? DEFAULT_MODELS.lmstudio : DEFAULT_MODELS[this.provider]);\n const defaultBaseUrl = this.provider === 'lmstudio' ? process.env.LMSTUDIO_URL ?? DEFAULT_BASE_URLS.lmstudio : DEFAULT_BASE_URLS[this.provider];\n this.baseUrl = (opts.baseUrl ?? defaultBaseUrl).replace(/\\/$/, '');\n\n if (this.provider === 'lmstudio') {\n // Local LM Studio server — no credential to check, mirrors client.ts's PROVIDER_CONFIGS.lmstudio.\n this.apiKey = opts.apiKey ?? 'not-needed';\n return;\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 const envVar = API_KEY_ENV_VARS[this.provider];\n return envVar ? process.env[envVar] : undefined;\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 * Image Client\n *\n * Single-purpose client for OpenRouter's image-generation API\n * (`POST {baseUrl}/images`). Raw `fetch`, no SDK — mirrors\n * `embedding-client.ts`'s shape (provider tables, retry policy, the\n * three response-error surfaces).\n *\n * @packageDocumentation\n */\n\nimport type { JsonValue } from '@almadar/core';\n\nexport type ImageProvider = 'openrouter';\n\nconst IMAGE_PROVIDERS: readonly ImageProvider[] = ['openrouter'];\n\n/** Canonical image-provider resolution, mirrors `resolveDefaultEmbeddingProvider`. */\nexport function resolveDefaultImageProvider(): ImageProvider {\n const v = process.env.IMAGE_PROVIDER;\n return v && (IMAGE_PROVIDERS as readonly string[]).includes(v) ? (v as ImageProvider) : 'openrouter';\n}\n\nexport const OPENROUTER_IMAGE_MODELS = {\n SEEDREAM_5_LITE: 'bytedance-seed/seedream-5-0-lite',\n SEEDREAM_4_5: 'bytedance-seed/seedream-4.5',\n GEMINI_3_1_FLASH_LITE_IMAGE: 'google/gemini-3.1-flash-lite-image',\n GEMINI_2_5_FLASH_IMAGE: 'google/gemini-2.5-flash-image',\n FLUX_2_PRO: 'black-forest-labs/flux.2-pro',\n} as const;\n\nexport const OPENROUTER_IMAGE_DEFAULTS = {\n sheet: OPENROUTER_IMAGE_MODELS.SEEDREAM_5_LITE,\n preview: OPENROUTER_IMAGE_MODELS.GEMINI_3_1_FLASH_LITE_IMAGE,\n} as const;\n\nexport interface ImageClientOptions {\n provider?: ImageProvider;\n model?: string;\n apiKey?: string;\n baseUrl?: string;\n fetchImpl?: typeof fetch;\n}\n\n/** An input reference image — `https:` or `data:` URL. Transport only, no fetching/validation. */\nexport interface ImageReference {\n url: string;\n}\n\nexport interface ImageGenerateOptions {\n prompt: string;\n model?: string;\n n?: number;\n size?: `${number}x${number}`;\n resolution?: '512' | '1K' | '2K' | '4K';\n aspectRatio?: string;\n quality?: 'auto' | 'low' | 'medium' | 'high';\n background?: 'auto' | 'transparent' | 'opaque';\n seed?: number;\n references?: ReadonlyArray<ImageReference>;\n provider?: { order?: string[]; only?: string[]; allowFallbacks?: boolean };\n}\n\nexport interface GeneratedImage {\n bytes: Uint8Array;\n mediaType: string;\n}\n\nexport interface ImageGenerateResult {\n images: GeneratedImage[];\n usage: { cost: number; totalTokens: number };\n model: string;\n dropped: string[];\n}\n\nexport interface ImageModelCapabilities {\n model: string;\n supportedParameters: ReadonlySet<string>;\n raw: JsonValue;\n}\n\n/** Transient-failure retry policy: attempts and backoff (ms) between them. */\nconst IMAGE_MAX_ATTEMPTS = 3;\nconst IMAGE_RETRY_BACKOFF_MS = [500, 2000];\n\nconst DEFAULT_MODELS: Record<ImageProvider, string> = {\n openrouter: OPENROUTER_IMAGE_DEFAULTS.sheet,\n};\n\nconst DEFAULT_BASE_URLS: Record<ImageProvider, string> = {\n openrouter: 'https://openrouter.ai/api/v1',\n};\n\nconst API_KEY_ENV_VARS: Record<ImageProvider, string> = {\n openrouter: 'OPEN_ROUTER_API_KEY',\n};\n\n/**\n * Request field → OpenRouter `supported_parameters` name. The single\n * place `generateChecked` reads to decide what to strip — change the\n * mapping here, not at each call site.\n */\nconst IMAGE_CHECKED_PARAM_NAMES = {\n background: 'background',\n seed: 'seed',\n references: 'input_references',\n} as const;\n\nfunction isJsonObject(value: JsonValue): value is { [key: string]: JsonValue } {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction asString(value: JsonValue | undefined): string | undefined {\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction asNumber(value: JsonValue | undefined): number | undefined {\n return typeof value === 'number' ? value : undefined;\n}\n\nfunction parseJsonBody(provider: ImageProvider, rawBody: string): JsonValue {\n try {\n return JSON.parse(rawBody) as JsonValue;\n } catch {\n throw new Error(`ImageClient: ${provider} returned unparseable body: ${rawBody}`);\n }\n}\n\nfunction decodeBase64(b64: string): Uint8Array {\n if (typeof globalThis.Buffer !== 'undefined') {\n return new Uint8Array(globalThis.Buffer.from(b64, 'base64'));\n }\n const binary = atob(b64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes;\n}\n\n/**\n * Collects the union of `supported_parameters` across a `.../endpoints`\n * response's `data`/`endpoints` arrays. Returns an empty set (never\n * throws) when the shape doesn't match — the caller still gets `raw`.\n */\nfunction collectSupportedParameters(body: JsonValue): ReadonlySet<string> {\n const params = new Set<string>();\n if (!isJsonObject(body)) return params;\n const candidateArrays: JsonValue[][] = [];\n if (Array.isArray(body.data)) candidateArrays.push(body.data);\n if (Array.isArray(body.endpoints)) candidateArrays.push(body.endpoints);\n for (const arr of candidateArrays) {\n for (const entry of arr) {\n if (!isJsonObject(entry)) continue;\n const sp = entry.supported_parameters;\n if (!Array.isArray(sp)) continue;\n for (const p of sp) if (typeof p === 'string') params.add(p);\n }\n }\n return params;\n}\n\nexport class ImageClient {\n private readonly provider: ImageProvider;\n private readonly model: string;\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n private readonly capabilitiesCache = new Map<string, ImageModelCapabilities>();\n\n constructor(opts: ImageClientOptions = {}) {\n this.provider = opts.provider ?? 'openrouter';\n this.model = opts.model ?? DEFAULT_MODELS[this.provider];\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URLS[this.provider]).replace(/\\/$/, '');\n this.fetchImpl = opts.fetchImpl ?? fetch;\n\n const apiKey = opts.apiKey ?? process.env[API_KEY_ENV_VARS[this.provider]];\n if (!apiKey) {\n throw new Error(\n `ImageClient: 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 /** Generate images. Never strips request fields — `dropped` is always `[]`. */\n async generate(opts: ImageGenerateOptions): Promise<ImageGenerateResult> {\n return this.performGenerate(opts, []);\n }\n\n /**\n * Per-model `.../endpoints` capabilities, memoized per instance per\n * model. `supportedParameters` is the empty set (not a throw) when the\n * response shape doesn't match what's expected.\n */\n async capabilities(modelId?: string): Promise<ImageModelCapabilities> {\n const model = modelId ?? this.model;\n const cached = this.capabilitiesCache.get(model);\n if (cached) return cached;\n\n const response = await this.fetchWithRetry(`${this.baseUrl}/images/models/${model}/endpoints`, {\n method: 'GET',\n headers: { Authorization: `Bearer ${this.apiKey}` },\n });\n if (!response.ok) {\n const errorText = await response.text().catch(() => '<no body>');\n throw new Error(\n `ImageClient: ${this.provider} returned ${response.status} ${response.statusText}: ${errorText}`,\n );\n }\n const body = parseJsonBody(this.provider, await response.text());\n const result: ImageModelCapabilities = { model, supportedParameters: collectSupportedParameters(body), raw: body };\n this.capabilitiesCache.set(model, result);\n return result;\n }\n\n /**\n * Like `generate`, but first reads `capabilities(model)` and strips\n * any of `background`/`seed`/`references` the model doesn't list in\n * `supported_parameters` — their (OpenRouter) names come back in\n * `dropped` rather than being silently swallowed.\n */\n async generateChecked(opts: ImageGenerateOptions): Promise<ImageGenerateResult> {\n const model = opts.model ?? this.model;\n const caps = await this.capabilities(model);\n const dropped: string[] = [];\n const filtered: ImageGenerateOptions = { ...opts, model };\n\n if (filtered.background !== undefined && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.background)) {\n delete filtered.background;\n dropped.push(IMAGE_CHECKED_PARAM_NAMES.background);\n }\n if (filtered.seed !== undefined && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.seed)) {\n delete filtered.seed;\n dropped.push(IMAGE_CHECKED_PARAM_NAMES.seed);\n }\n if (\n filtered.references !== undefined &&\n filtered.references.length > 0 &&\n !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.references)\n ) {\n delete filtered.references;\n dropped.push(IMAGE_CHECKED_PARAM_NAMES.references);\n }\n\n return this.performGenerate(filtered, dropped);\n }\n\n private async performGenerate(opts: ImageGenerateOptions, dropped: string[]): Promise<ImageGenerateResult> {\n if (opts.size !== undefined && (opts.resolution !== undefined || opts.aspectRatio !== undefined)) {\n throw new Error('ImageClient.generate: `size` cannot be combined with `resolution`/`aspectRatio` — pass one or the other');\n }\n const model = opts.model ?? this.model;\n const requestBody: Record<string, JsonValue> = { model, prompt: opts.prompt, output_format: 'png' };\n if (opts.n !== undefined) requestBody.n = opts.n;\n if (opts.size !== undefined) requestBody.size = opts.size;\n if (opts.resolution !== undefined) requestBody.resolution = opts.resolution;\n if (opts.aspectRatio !== undefined) requestBody.aspect_ratio = opts.aspectRatio;\n if (opts.quality !== undefined) requestBody.quality = opts.quality;\n if (opts.background !== undefined) requestBody.background = opts.background;\n if (opts.seed !== undefined) requestBody.seed = opts.seed;\n if (opts.references !== undefined && opts.references.length > 0) {\n requestBody.input_references = opts.references.map((r) => ({ type: 'image_url', image_url: { url: r.url } }));\n }\n if (opts.provider !== undefined) {\n const providerField: Record<string, JsonValue> = {};\n if (opts.provider.order !== undefined) providerField.order = opts.provider.order;\n if (opts.provider.only !== undefined) providerField.only = opts.provider.only;\n if (opts.provider.allowFallbacks !== undefined) providerField.allow_fallbacks = opts.provider.allowFallbacks;\n requestBody.provider = providerField;\n }\n\n const response = await this.fetchWithRetry(`${this.baseUrl}/images`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.apiKey}` },\n body: JSON.stringify(requestBody),\n });\n if (!response.ok) {\n const errorText = await response.text().catch(() => '<no body>');\n throw new Error(\n `ImageClient: ${this.provider} returned ${response.status} ${response.statusText}: ${errorText}`,\n );\n }\n const rawBody = await response.text();\n const body = parseJsonBody(this.provider, rawBody);\n if (!isJsonObject(body)) {\n throw new Error(`ImageClient: ${this.provider} returned unparseable body: ${rawBody}`);\n }\n if (isJsonObject(body.error)) {\n const detail = asString(body.error.message) ?? rawBody;\n throw new Error(`ImageClient: ${this.provider} returned an error: ${detail}`);\n }\n if (!Array.isArray(body.data)) {\n throw new Error(`ImageClient: ${this.provider} response has no \\`data\\` array: ${rawBody}`);\n }\n\n const images: GeneratedImage[] = body.data.map((entry, i) => {\n if (!isJsonObject(entry)) {\n throw new Error(`ImageClient: ${this.provider} response data[${i}] is not an object: ${rawBody}`);\n }\n const b64 = asString(entry.b64_json);\n if (!b64) {\n throw new Error(`ImageClient: ${this.provider} response data[${i}] missing \\`b64_json\\`: ${rawBody}`);\n }\n return { bytes: decodeBase64(b64), mediaType: asString(entry.media_type) ?? 'image/png' };\n });\n\n const usageObj = isJsonObject(body.usage) ? body.usage : undefined;\n const usage = {\n cost: asNumber(usageObj?.cost) ?? 0,\n totalTokens: asNumber(usageObj?.total_tokens) ?? 0,\n };\n\n return { images, usage, model, dropped };\n }\n\n /** Retries on network failure or a 5xx status; any other response is returned as-is. */\n private async fetchWithRetry(url: string, init: RequestInit): Promise<Response> {\n let lastNetworkError: Error | undefined;\n let response: Response | undefined;\n for (let attempt = 1; attempt <= IMAGE_MAX_ATTEMPTS; attempt++) {\n try {\n response = await this.fetchImpl(url, init);\n } catch (e) {\n response = undefined;\n lastNetworkError = e instanceof Error ? e : new Error(String(e));\n }\n if (response !== undefined && response.status < 500) return response;\n const backoff = IMAGE_RETRY_BACKOFF_MS[attempt - 1];\n if (backoff === undefined) break;\n await new Promise((r) => setTimeout(r, backoff));\n }\n if (response) return response;\n throw lastNetworkError ?? new Error(`ImageClient: ${this.provider} request failed after ${IMAGE_MAX_ATTEMPTS} attempts`);\n }\n}\n\nexport const IMAGE_PROVIDER_CATALOG: ReadonlyArray<{ provider: ImageProvider; models: string[]; defaultModel: string }> = [\n {\n provider: 'openrouter',\n models: Object.values(OPENROUTER_IMAGE_MODELS),\n defaultModel: OPENROUTER_IMAGE_DEFAULTS.sheet,\n },\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,IAAM,sBAAoD,CAAC,UAAU,cAAc,UAAU;AAiBtF,SAAS,kCAAqD;AACnE,QAAM,IAAI,QAAQ,IAAI;AACtB,SAAO,KAAM,oBAA0C,SAAS,CAAC,IAAK,IAA0B;AAClG;AAmCA,IAAM,qBAAqB;AAC3B,IAAM,yBAAyB,CAAC,KAAK,GAAI;AAGzC,IAAM,oBACJ;AAEF,IAAM,iBAAoD;AAAA,EACxD,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,UAAU;AACZ;AAEA,IAAM,oBAAuD;AAAA,EAC3D,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,UAAU;AACZ;AAEA,IAAM,mBAA+D;AAAA,EACnE,QAAQ;AAAA,EACR,YAAY;AACd;AAEO,IAAM,kBAAN,MAAsB;AAAA,EAM3B,YAAY,MAA8B;AACxC,SAAK,WAAW,KAAK;AACrB,SAAK,QAAQ,KAAK,UAAU,KAAK,aAAa,aAAa,QAAQ,IAAI,4BAA4B,eAAe,WAAW,eAAe,KAAK,QAAQ;AACzJ,UAAM,iBAAiB,KAAK,aAAa,aAAa,QAAQ,IAAI,gBAAgB,kBAAkB,WAAW,kBAAkB,KAAK,QAAQ;AAC9I,SAAK,WAAW,KAAK,WAAW,gBAAgB,QAAQ,OAAO,EAAE;AAEjE,QAAI,KAAK,aAAa,YAAY;AAEhC,WAAK,SAAS,KAAK,UAAU;AAC7B;AAAA,IACF;AACA,UAAM,SAAS,KAAK,UAAU,KAAK,cAAc;AACjD,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI;AAAA,QACR,0CAA0C,KAAK,QAAQ,oBAC9C,iBAAiB,KAAK,QAAQ,CAAC;AAAA,MAC1C;AAAA,IACF;AACA,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA,EAGA,MAAM,MAAM,MAAwC;AAClD,UAAM,QAAQ,MAAM,KAAK,WAAW,CAAC,IAAI,CAAC;AAC1C,UAAM,YAAY,MAAM,WAAW,CAAC;AACpC,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,MAAM,uDAAuD;AAAA,IACzE;AACA,WAAO,EAAE,WAAW,OAAO,MAAM,MAAM;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,WAAW,OAA6D;AAC5E,QAAI,MAAM,WAAW,GAAG;AACtB,aAAO,EAAE,YAAY,CAAC,GAAG,OAAO,EAAE,cAAc,GAAG,aAAa,EAAE,EAAE;AAAA,IACtE;AAOA,QAAI,UAAU;AACd,QAAI,YAAmB,IAAI,MAAM,8BAA8B;AAC/D,aAAS,UAAU,GAAG,WAAW,oBAAoB,WAAW;AAC9D,YAAM,UAAU,MAAM,IAAI,CAAC,MAAO,EAAE,SAAS,UAAU,EAAE,MAAM,GAAG,OAAO,IAAI,CAAE;AAC/E,UAAI;AACF,eAAO,MAAM,KAAK,eAAe,MAAM,QAAQ,OAAO;AAAA,MACxD,SAAS,GAAG;AACV,oBAAY,aAAa,QAAQ,IAAI,IAAI,MAAM,OAAO,CAAC,CAAC;AACxD,YAAI,kBAAkB,KAAK,UAAU,OAAO,KAAK,UAAU,KAAK;AAC9D,oBAAU,KAAK,MAAM,UAAU,CAAC;AAChC;AAAA,QACF;AACA,cAAM,UAAU,uBAAuB,UAAU,CAAC;AAClD,YAAI,YAAY,OAAW;AAC3B,cAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC;AAAA,MACjD;AAAA,IACF;AACA,UAAM;AAAA,EACR;AAAA,EAEA,MAAc,eACZ,YACA,SAC+B;AAC/B,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,eAAe;AAAA,MACzD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,eAAe,UAAU,KAAK,MAAM;AAAA,MACtC;AAAA,MACA,MAAM,KAAK,UAAU,EAAE,OAAO,KAAK,OAAO,OAAO,QAAQ,CAAC;AAAA,IAC5D,CAAC;AACD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,WAAW;AAC/D,YAAM,IAAI;AAAA,QACR,oBAAoB,KAAK,QAAQ,aAAa,SAAS,MAAM,IAAI,SAAS,UAAU,KAAK,SAAS;AAAA,MACpG;AAAA,IACF;AACA,UAAM,UAAU,MAAM,SAAS,KAAK;AACpC,QAAI;AACJ,QAAI;AACF,aAAO,KAAK,MAAM,OAAO;AAAA,IAC3B,QAAQ;AACN,YAAM,IAAI;AAAA,QACR,oBAAoB,KAAK,QAAQ,+BAA+B,OAAO;AAAA,MACzE;AAAA,IACF;AACA,QAAI,CAAC,MAAM,QAAQ,KAAK,IAAI,GAAG;AAI7B,YAAM,SAAS,KAAK,OAAO,WAAW;AACtC,YAAM,IAAI;AAAA,QACR,oBAAoB,KAAK,QAAQ,oCAAoC,MAAM;AAAA,MAC7E;AAAA,IACF;AACA,UAAM,UAAU,CAAC,GAAG,KAAK,IAAI,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAC/D,QAAI,QAAQ,WAAW,YAAY;AACjC,YAAM,IAAI;AAAA,QACR,sCAAsC,QAAQ,MAAM,mBAAmB,UAAU;AAAA,MACnF;AAAA,IACF;AACA,WAAO;AAAA,MACL,YAAY,QAAQ,IAAI,CAAC,MAAM,EAAE,SAAS;AAAA,MAC1C,OAAO;AAAA,QACL,cAAc,KAAK,OAAO,iBAAiB;AAAA,QAC3C,aAAa,KAAK,OAAO,gBAAgB;AAAA,MAC3C;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,gBAAoC;AAC1C,UAAM,SAAS,iBAAiB,KAAK,QAAQ;AAC7C,WAAO,SAAS,QAAQ,IAAI,MAAM,IAAI;AAAA,EACxC;AACF;AAWO,SAAS,iBAAiB,GAA0B,GAAkC;AAC3F,MAAI,EAAE,WAAW,EAAE,QAAQ;AACzB,UAAM,IAAI,MAAM,6CAA6C,EAAE,MAAM,OAAO,EAAE,MAAM,GAAG;AAAA,EACzF;AACA,MAAI,MAAM;AACV,MAAI,QAAQ;AACZ,MAAI,QAAQ;AACZ,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,UAAM,KAAK,EAAE,CAAC;AACd,UAAM,KAAK,EAAE,CAAC;AACd,WAAO,KAAK;AACZ,aAAS,KAAK;AACd,aAAS,KAAK;AAAA,EAChB;AACA,MAAI,UAAU,KAAK,UAAU,EAAG,QAAO;AACvC,SAAO,OAAO,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK;AAClD;;;ACtPA,IAAM,kBAA4C,CAAC,YAAY;AAGxD,SAAS,8BAA6C;AAC3D,QAAM,IAAI,QAAQ,IAAI;AACtB,SAAO,KAAM,gBAAsC,SAAS,CAAC,IAAK,IAAsB;AAC1F;AAEO,IAAM,0BAA0B;AAAA,EACrC,iBAAiB;AAAA,EACjB,cAAc;AAAA,EACd,6BAA6B;AAAA,EAC7B,wBAAwB;AAAA,EACxB,YAAY;AACd;AAEO,IAAM,4BAA4B;AAAA,EACvC,OAAO,wBAAwB;AAAA,EAC/B,SAAS,wBAAwB;AACnC;AAgDA,IAAM,qBAAqB;AAC3B,IAAM,yBAAyB,CAAC,KAAK,GAAI;AAEzC,IAAMA,kBAAgD;AAAA,EACpD,YAAY,0BAA0B;AACxC;AAEA,IAAMC,qBAAmD;AAAA,EACvD,YAAY;AACd;AAEA,IAAMC,oBAAkD;AAAA,EACtD,YAAY;AACd;AAOA,IAAM,4BAA4B;AAAA,EAChC,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,YAAY;AACd;AAEA,SAAS,aAAa,OAAyD;AAC7E,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;AAEA,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;AAEA,SAAS,cAAc,UAAyB,SAA4B;AAC1E,MAAI;AACF,WAAO,KAAK,MAAM,OAAO;AAAA,EAC3B,QAAQ;AACN,UAAM,IAAI,MAAM,gBAAgB,QAAQ,+BAA+B,OAAO,EAAE;AAAA,EAClF;AACF;AAEA,SAAS,aAAa,KAAyB;AAC7C,MAAI,OAAO,WAAW,WAAW,aAAa;AAC5C,WAAO,IAAI,WAAW,WAAW,OAAO,KAAK,KAAK,QAAQ,CAAC;AAAA,EAC7D;AACA,QAAM,SAAS,KAAK,GAAG;AACvB,QAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;AAC1C,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,IAAK,OAAM,CAAC,IAAI,OAAO,WAAW,CAAC;AACtE,SAAO;AACT;AAOA,SAAS,2BAA2B,MAAsC;AACxE,QAAM,SAAS,oBAAI,IAAY;AAC/B,MAAI,CAAC,aAAa,IAAI,EAAG,QAAO;AAChC,QAAM,kBAAiC,CAAC;AACxC,MAAI,MAAM,QAAQ,KAAK,IAAI,EAAG,iBAAgB,KAAK,KAAK,IAAI;AAC5D,MAAI,MAAM,QAAQ,KAAK,SAAS,EAAG,iBAAgB,KAAK,KAAK,SAAS;AACtE,aAAW,OAAO,iBAAiB;AACjC,eAAW,SAAS,KAAK;AACvB,UAAI,CAAC,aAAa,KAAK,EAAG;AAC1B,YAAM,KAAK,MAAM;AACjB,UAAI,CAAC,MAAM,QAAQ,EAAE,EAAG;AACxB,iBAAW,KAAK,GAAI,KAAI,OAAO,MAAM,SAAU,QAAO,IAAI,CAAC;AAAA,IAC7D;AAAA,EACF;AACA,SAAO;AACT;AAEO,IAAM,cAAN,MAAkB;AAAA,EAQvB,YAAY,OAA2B,CAAC,GAAG;AAF3C,SAAiB,oBAAoB,oBAAI,IAAoC;AAG3E,SAAK,WAAW,KAAK,YAAY;AACjC,SAAK,QAAQ,KAAK,SAASF,gBAAe,KAAK,QAAQ;AACvD,SAAK,WAAW,KAAK,WAAWC,mBAAkB,KAAK,QAAQ,GAAG,QAAQ,OAAO,EAAE;AACnF,SAAK,YAAY,KAAK,aAAa;AAEnC,UAAM,SAAS,KAAK,UAAU,QAAQ,IAAIC,kBAAiB,KAAK,QAAQ,CAAC;AACzE,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI;AAAA,QACR,sCAAsC,KAAK,QAAQ,oBAC1CA,kBAAiB,KAAK,QAAQ,CAAC;AAAA,MAC1C;AAAA,IACF;AACA,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA,EAGA,MAAM,SAAS,MAA0D;AACvE,WAAO,KAAK,gBAAgB,MAAM,CAAC,CAAC;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,aAAa,SAAmD;AACpE,UAAM,QAAQ,WAAW,KAAK;AAC9B,UAAM,SAAS,KAAK,kBAAkB,IAAI,KAAK;AAC/C,QAAI,OAAQ,QAAO;AAEnB,UAAM,WAAW,MAAM,KAAK,eAAe,GAAG,KAAK,OAAO,kBAAkB,KAAK,cAAc;AAAA,MAC7F,QAAQ;AAAA,MACR,SAAS,EAAE,eAAe,UAAU,KAAK,MAAM,GAAG;AAAA,IACpD,CAAC;AACD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,WAAW;AAC/D,YAAM,IAAI;AAAA,QACR,gBAAgB,KAAK,QAAQ,aAAa,SAAS,MAAM,IAAI,SAAS,UAAU,KAAK,SAAS;AAAA,MAChG;AAAA,IACF;AACA,UAAM,OAAO,cAAc,KAAK,UAAU,MAAM,SAAS,KAAK,CAAC;AAC/D,UAAM,SAAiC,EAAE,OAAO,qBAAqB,2BAA2B,IAAI,GAAG,KAAK,KAAK;AACjH,SAAK,kBAAkB,IAAI,OAAO,MAAM;AACxC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,gBAAgB,MAA0D;AAC9E,UAAM,QAAQ,KAAK,SAAS,KAAK;AACjC,UAAM,OAAO,MAAM,KAAK,aAAa,KAAK;AAC1C,UAAM,UAAoB,CAAC;AAC3B,UAAM,WAAiC,EAAE,GAAG,MAAM,MAAM;AAExD,QAAI,SAAS,eAAe,UAAa,CAAC,KAAK,oBAAoB,IAAI,0BAA0B,UAAU,GAAG;AAC5G,aAAO,SAAS;AAChB,cAAQ,KAAK,0BAA0B,UAAU;AAAA,IACnD;AACA,QAAI,SAAS,SAAS,UAAa,CAAC,KAAK,oBAAoB,IAAI,0BAA0B,IAAI,GAAG;AAChG,aAAO,SAAS;AAChB,cAAQ,KAAK,0BAA0B,IAAI;AAAA,IAC7C;AACA,QACE,SAAS,eAAe,UACxB,SAAS,WAAW,SAAS,KAC7B,CAAC,KAAK,oBAAoB,IAAI,0BAA0B,UAAU,GAClE;AACA,aAAO,SAAS;AAChB,cAAQ,KAAK,0BAA0B,UAAU;AAAA,IACnD;AAEA,WAAO,KAAK,gBAAgB,UAAU,OAAO;AAAA,EAC/C;AAAA,EAEA,MAAc,gBAAgB,MAA4B,SAAiD;AACzG,QAAI,KAAK,SAAS,WAAc,KAAK,eAAe,UAAa,KAAK,gBAAgB,SAAY;AAChG,YAAM,IAAI,MAAM,8GAAyG;AAAA,IAC3H;AACA,UAAM,QAAQ,KAAK,SAAS,KAAK;AACjC,UAAM,cAAyC,EAAE,OAAO,QAAQ,KAAK,QAAQ,eAAe,MAAM;AAClG,QAAI,KAAK,MAAM,OAAW,aAAY,IAAI,KAAK;AAC/C,QAAI,KAAK,SAAS,OAAW,aAAY,OAAO,KAAK;AACrD,QAAI,KAAK,eAAe,OAAW,aAAY,aAAa,KAAK;AACjE,QAAI,KAAK,gBAAgB,OAAW,aAAY,eAAe,KAAK;AACpE,QAAI,KAAK,YAAY,OAAW,aAAY,UAAU,KAAK;AAC3D,QAAI,KAAK,eAAe,OAAW,aAAY,aAAa,KAAK;AACjE,QAAI,KAAK,SAAS,OAAW,aAAY,OAAO,KAAK;AACrD,QAAI,KAAK,eAAe,UAAa,KAAK,WAAW,SAAS,GAAG;AAC/D,kBAAY,mBAAmB,KAAK,WAAW,IAAI,CAAC,OAAO,EAAE,MAAM,aAAa,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;AAAA,IAC9G;AACA,QAAI,KAAK,aAAa,QAAW;AAC/B,YAAM,gBAA2C,CAAC;AAClD,UAAI,KAAK,SAAS,UAAU,OAAW,eAAc,QAAQ,KAAK,SAAS;AAC3E,UAAI,KAAK,SAAS,SAAS,OAAW,eAAc,OAAO,KAAK,SAAS;AACzE,UAAI,KAAK,SAAS,mBAAmB,OAAW,eAAc,kBAAkB,KAAK,SAAS;AAC9F,kBAAY,WAAW;AAAA,IACzB;AAEA,UAAM,WAAW,MAAM,KAAK,eAAe,GAAG,KAAK,OAAO,WAAW;AAAA,MACnE,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oBAAoB,eAAe,UAAU,KAAK,MAAM,GAAG;AAAA,MACtF,MAAM,KAAK,UAAU,WAAW;AAAA,IAClC,CAAC;AACD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,WAAW;AAC/D,YAAM,IAAI;AAAA,QACR,gBAAgB,KAAK,QAAQ,aAAa,SAAS,MAAM,IAAI,SAAS,UAAU,KAAK,SAAS;AAAA,MAChG;AAAA,IACF;AACA,UAAM,UAAU,MAAM,SAAS,KAAK;AACpC,UAAM,OAAO,cAAc,KAAK,UAAU,OAAO;AACjD,QAAI,CAAC,aAAa,IAAI,GAAG;AACvB,YAAM,IAAI,MAAM,gBAAgB,KAAK,QAAQ,+BAA+B,OAAO,EAAE;AAAA,IACvF;AACA,QAAI,aAAa,KAAK,KAAK,GAAG;AAC5B,YAAM,SAAS,SAAS,KAAK,MAAM,OAAO,KAAK;AAC/C,YAAM,IAAI,MAAM,gBAAgB,KAAK,QAAQ,uBAAuB,MAAM,EAAE;AAAA,IAC9E;AACA,QAAI,CAAC,MAAM,QAAQ,KAAK,IAAI,GAAG;AAC7B,YAAM,IAAI,MAAM,gBAAgB,KAAK,QAAQ,oCAAoC,OAAO,EAAE;AAAA,IAC5F;AAEA,UAAM,SAA2B,KAAK,KAAK,IAAI,CAAC,OAAO,MAAM;AAC3D,UAAI,CAAC,aAAa,KAAK,GAAG;AACxB,cAAM,IAAI,MAAM,gBAAgB,KAAK,QAAQ,kBAAkB,CAAC,uBAAuB,OAAO,EAAE;AAAA,MAClG;AACA,YAAM,MAAM,SAAS,MAAM,QAAQ;AACnC,UAAI,CAAC,KAAK;AACR,cAAM,IAAI,MAAM,gBAAgB,KAAK,QAAQ,kBAAkB,CAAC,2BAA2B,OAAO,EAAE;AAAA,MACtG;AACA,aAAO,EAAE,OAAO,aAAa,GAAG,GAAG,WAAW,SAAS,MAAM,UAAU,KAAK,YAAY;AAAA,IAC1F,CAAC;AAED,UAAM,WAAW,aAAa,KAAK,KAAK,IAAI,KAAK,QAAQ;AACzD,UAAM,QAAQ;AAAA,MACZ,MAAM,SAAS,UAAU,IAAI,KAAK;AAAA,MAClC,aAAa,SAAS,UAAU,YAAY,KAAK;AAAA,IACnD;AAEA,WAAO,EAAE,QAAQ,OAAO,OAAO,QAAQ;AAAA,EACzC;AAAA;AAAA,EAGA,MAAc,eAAe,KAAa,MAAsC;AAC9E,QAAI;AACJ,QAAI;AACJ,aAAS,UAAU,GAAG,WAAW,oBAAoB,WAAW;AAC9D,UAAI;AACF,mBAAW,MAAM,KAAK,UAAU,KAAK,IAAI;AAAA,MAC3C,SAAS,GAAG;AACV,mBAAW;AACX,2BAAmB,aAAa,QAAQ,IAAI,IAAI,MAAM,OAAO,CAAC,CAAC;AAAA,MACjE;AACA,UAAI,aAAa,UAAa,SAAS,SAAS,IAAK,QAAO;AAC5D,YAAM,UAAU,uBAAuB,UAAU,CAAC;AAClD,UAAI,YAAY,OAAW;AAC3B,YAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC;AAAA,IACjD;AACA,QAAI,SAAU,QAAO;AACrB,UAAM,oBAAoB,IAAI,MAAM,gBAAgB,KAAK,QAAQ,yBAAyB,kBAAkB,WAAW;AAAA,EACzH;AACF;AAEO,IAAM,yBAA6G;AAAA,EACxH;AAAA,IACE,UAAU;AAAA,IACV,QAAQ,OAAO,OAAO,uBAAuB;AAAA,IAC7C,cAAc,0BAA0B;AAAA,EAC1C;AACF;;;ACzSO,SAAS,iBACd,UACA,cACkB;AAClB,MAAI,iBAAiB,UAAU;AAC7B,UAAMC,eAAc,cAAc,QAAQ;AAC1C,WAAO;AAAA,MACL,aAAa;AAAA,MACb,QAAQ;AAAA,MACR,gBAAgB;AAAA,MAChB,qBAAqB,wBAAwB,QAAQ;AAAA,MACrD,sBAAsBA,aAAY;AAAA,MAClC,oBAAoBA,aAAY;AAAA,IAClC;AAAA,EACF;AAEA,MAAI;AACF,SAAK,MAAM,QAAQ;AACnB,WAAO,EAAE,aAAa,OAAO,QAAQ,OAAO;AAAA,EAC9C,QAAQ;AAAA,EAER;AAEA,MAAI,iBAAiB,UAAU,iBAAiB,MAAM;AACpD,UAAM,UAAU,SAAS,KAAK;AAE9B,UAAM,eACJ,QAAQ,SAAS,GAAG,KACpB,QAAQ,SAAS,GAAG,KACpB,QAAQ,SAAS,KAAK,KACtB,QAAQ,KAAK,OAAO,KACpB,QAAQ,KAAK,OAAO;AAEtB,QAAI,cAAc;AAChB,YAAMA,eAAc,cAAc,QAAQ;AAC1C,aAAO;AAAA,QACL,aAAa;AAAA,QACb,QAAQ;AAAA,QACR,gBAAgB;AAAA,QAChB,qBAAqB,wBAAwB,QAAQ;AAAA,QACrD,sBAAsBA,aAAY;AAAA,QAClC,oBAAoBA,aAAY;AAAA,MAClC;AAAA,IACF;AAEA,QAAI;AACF,YAAM,SAAS,cAAc,OAAO;AACpC,WAAK,MAAM,MAAM;AACjB,aAAO,EAAE,aAAa,OAAO,QAAQ,OAAO;AAAA,IAC9C,QAAQ;AACN,aAAO,EAAE,aAAa,OAAO,QAAQ,OAAO;AAAA,IAC9C;AAAA,EACF;AAEA,QAAM,cAAc,cAAc,QAAQ;AAC1C,MACE,YAAY,uBAAuB,KACnC,YAAY,qBAAqB,GACjC;AACA,WAAO;AAAA,MACL,aAAa;AAAA,MACb,QAAQ;AAAA,MACR,gBAAgB;AAAA,MAChB,qBAAqB,wBAAwB,QAAQ;AAAA,MACrD,sBAAsB,YAAY;AAAA,MAClC,oBAAoB,YAAY;AAAA,IAClC;AAAA,EACF;AAEA,SAAO,EAAE,aAAa,OAAO,QAAQ,OAAO;AAC9C;AAMA,SAAS,cAAc,MAOrB;AACA,MAAI,WAAW;AACf,MAAI,UAAU;AACd,MAAI,eAAe;AACnB,MAAI,gBAAgB;AACpB,MAAI,aAAa;AACjB,MAAI,cAAc;AAElB,aAAW,QAAQ,MAAM;AACvB,QAAI,SAAS;AACX,gBAAU;AACV;AAAA,IACF;AACA,QAAI,SAAS,QAAQ,UAAU;AAC7B,gBAAU;AACV;AAAA,IACF;AACA,QAAI,SAAS,KAAK;AAChB,iBAAW,CAAC;AACZ;AAAA,IACF;AACA,QAAI,SAAU;AAEd,YAAQ,MAAM;AAAA,MACZ,KAAK;AACH;AACA;AAAA,MACF,KAAK;AACH;AACA;AAAA,MACF,KAAK;AACH;AACA;AAAA,MACF,KAAK;AACH;AACA;AAAA,IACJ;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,sBAAsB,KAAK,IAAI,GAAG,eAAe,aAAa;AAAA,IAC9D,oBAAoB,KAAK,IAAI,GAAG,aAAa,WAAW;AAAA,EAC1D;AACF;AAEO,SAAS,wBAAwB,MAA8B;AACpE,QAAM,aAAa,cAAc,IAAI;AACrC,MAAI;AACF,WAAO,KAAK,MAAM,UAAU;AAAA,EAC9B,QAAQ;AAAA,EAER;AAEA,QAAM,UAAU,KAAK,KAAK;AAE1B,MAAI,QAAQ,WAAW,GAAG,GAAG;AAC3B,UAAM,oBAAoB,6BAA6B,OAAO;AAC9D,QAAI,oBAAoB,GAAG;AACzB,YAAM,SAAS,QAAQ,UAAU,GAAG,iBAAiB,IAAI;AACzD,UAAI;AACF,eAAO,KAAK,MAAM,MAAM;AAAA,MAC1B,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,MAAI,QAAQ,WAAW,GAAG,GAAG;AAC3B,UAAM,SAAS,cAAc,OAAO;AACpC,QAAI;AACF,aAAO,KAAK,MAAM,MAAM;AAAA,IAC1B,QAAQ;AACN,YAAM,oBAAoB,+BAA+B,OAAO;AAChE,UAAI,oBAAoB,GAAG;AACzB,cAAM,SAAS,QAAQ,UAAU,GAAG,iBAAiB,IAAI;AACzD,YAAI;AACF,iBAAO,KAAK,MAAM,MAAM;AAAA,QAC1B,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,6BAA6B,MAAsB;AAC1D,MAAI,QAAQ;AACZ,MAAI,WAAW;AACf,MAAI,UAAU;AACd,MAAI,yBAAyB;AAE7B,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,OAAO,KAAK,CAAC;AAEnB,QAAI,SAAS;AACX,gBAAU;AACV;AAAA,IACF;AACA,QAAI,SAAS,QAAQ,UAAU;AAC7B,gBAAU;AACV;AAAA,IACF;AACA,QAAI,SAAS,KAAK;AAChB,iBAAW,CAAC;AACZ;AAAA,IACF;AACA,QAAI,SAAU;AAEd,QAAI,SAAS,OAAO,SAAS,KAAK;AAChC;AAAA,IACF,WAAW,SAAS,OAAO,SAAS,KAAK;AACvC;AACA,UAAI,UAAU,GAAG;AACf,iCAAyB,IAAI;AAAA,MAC/B;AAAA,IACF,WAAW,SAAS,OAAO,UAAU,GAAG;AACtC,+BAAyB;AAAA,IAC3B;AAAA,EACF;AAEA,SAAO,yBAAyB,IAAI,yBAAyB;AAC/D;AAEA,SAAS,+BAA+B,MAAsB;AAC5D,MAAI,QAAQ;AACZ,MAAI,WAAW;AACf,MAAI,UAAU;AACd,MAAI,iBAAiB;AAErB,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,OAAO,KAAK,CAAC;AAEnB,QAAI,SAAS;AACX,gBAAU;AACV;AAAA,IACF;AACA,QAAI,SAAS,QAAQ,UAAU;AAC7B,gBAAU;AACV;AAAA,IACF;AACA,QAAI,SAAS,KAAK;AAChB,iBAAW,CAAC;AACZ;AAAA,IACF;AACA,QAAI,SAAU;AAEd,QAAI,SAAS,OAAO,SAAS,KAAK;AAChC;AAAA,IACF,WAAW,SAAS,OAAO,SAAS,KAAK;AACvC;AAAA,IACF,WAAW,SAAS,OAAO,UAAU,GAAG;AACtC,uBAAiB;AAAA,IACnB;AAAA,EACF;AAEA,SAAO,iBAAiB,IAAI,iBAAiB;AAC/C;AAEO,SAAS,kBAAkB,SAA0B;AAC1D,QAAM,UAAU,QAAQ,KAAK;AAC7B,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,WAAW,cAAc,OAAO;AACtC,MACE,SAAS,uBAAuB,KAChC,SAAS,qBAAqB,GAC9B;AACA,WAAO;AAAA,EACT;AAEA,QAAM,gBAAgB;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,aAAW,WAAW,eAAe;AACnC,QAAI,QAAQ,KAAK,OAAO,EAAG,QAAO;AAAA,EACpC;AAEA,SAAO;AACT;;;AC7QA,IAAM,qBAAqB;AAC3B,IAAM,4BAA4B;AAMlC,IAAM,qCAAqC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBpC,SAAS,eACd,UACA,cACQ;AACR,QAAM,cAAc,SAAS,QAAQ;AACrC,QAAM,cAAc,aAAa,UAAU;AAE3C,MAAI,cAAc,YACf,QAAQ,iBAAiB,EAAE,EAC3B,QAAQ,YAAY,EAAE,EACtB,KAAK;AAER,MAAI,YAAY,WAAW,GAAG,GAAG;AAC/B,QAAI;AACF,YAAM,aAAa,KAAK,MAAM,cAAc,WAAW,CAAC;AACxD,YAAM,OAAO,OAAO,KAAK,UAAU;AACnC,UAAI,KAAK,WAAW,KAAK,MAAM,QAAQ,WAAW,KAAK,CAAC,CAAC,CAAC,GAAG;AAC3D,sBAAc,WAAW,KAAK,CAAC,CAAC,EAC7B,IAAI,CAAC,SAAkB,KAAK,UAAU,IAAI,CAAC,EAC3C,KAAK,KAAK;AAAA,MACf;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,YAAY,WAAW,GAAG,KAAK,YAAY,WAAW,GAAG,GAAG;AAC9D,WAAO,cAAc;AAAA,EACvB;AAEA,QAAM,oBAAoB,cAAc,KAAK,WAAW;AACxD,QAAM,sBAAsB,YAAY,KAAK,WAAW;AAExD,MAAI,qBAAqB,qBAAqB;AAC5C,WAAO,cAAc,QAAQ;AAAA,EAC/B;AAEA,SAAO,cAAc;AACvB;AAEO,SAAS,uBAA0B,aAA+B;AACvE,UAAQ,KAAK,uDAAuD;AAEpE,MAAI;AACF,UAAM,kBAAkB,oBAAoB,WAAW,KAAK;AAC5D,UAAM,SAAS,cAAc,eAAe;AAC5C,UAAM,SAAS,KAAK,MAAM,MAAM;AAChC,YAAQ,IAAI,uDAAuD;AACnE,WAAO;AAAA,EACT,SAAS,OAAO;AACd,YAAQ,MAAM,8CAA8C,KAAK;AAAA,EACnE;AAEA,SAAO;AACT;AAgBA,eAAsB,qBACpB,SACgC;AAChC,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,mBAAmB;AAAA,IACnB;AAAA,IACA,2BAA2B;AAAA,EAC7B,IAAI;AAEJ,MAAI,cAAc;AAClB,MAAI,oBAAoB;AACxB,QAAM,WAAqB,CAAC;AAC5B,MAAI,cAAc;AAElB,UAAQ,IAAI,4DAA4D;AACxE,UAAQ;AAAA,IACN,8BAA8B,SAAS,wBAAwB,gBAAgB;AAAA,EACjF;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,OAAO,oBAAoB;AAAA,MAChD;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,kBAAc,oBAAoB,SAAS,GAAG,KAAK,SAAS;AAE5D,YAAQ;AAAA,MACN,oCAAoC,YAAY,MAAM,0BAA0B,SAAS,YAAY;AAAA,IACvG;AAEA,QAAI,aAAa,iBAAiB,aAAa,SAAS,YAAY;AAEpE,WAAO,WAAW,eAAe,oBAAoB,kBAAkB;AACrE;AACA,YAAM,aAAa,uBAAuB,WAAW,MAAM,0BAA0B,iBAAiB,IAAI,gBAAgB;AAC1H,cAAQ,IAAI,kBAAkB,UAAU,EAAE;AAC1C,eAAS,KAAK,UAAU;AAExB,YAAM,aAAa;AAAA,QACjB;AAAA,QACA;AAAA,MACF;AAEA,YAAM,eAAe,MAAM,OAAO,oBAAoB;AAAA,QACpD,cAAc;AAAA,QACd,YAAY;AAAA,QACZ;AAAA,MACF,CAAC;AAED,cAAQ;AAAA,QACN,yCAAyC,aAAa,IAAI,MAAM,0BAA0B,aAAa,YAAY;AAAA,MACrH;AAEA,YAAM,sBACJ,oBAAoB,aAAa,GAAG,KAAK,aAAa;AACxD,oBAAc,eAAe,aAAa,mBAAmB;AAE7D,mBAAa,iBAAiB,aAAa,aAAa,YAAY;AAAA,IACtE;AAEA,QACE,qBAAqB,oBACrB,WAAW,aACX;AACA,cAAQ;AAAA,QACN,6CAA6C,gBAAgB;AAAA,MAC/D;AACA,eAAS;AAAA,QACP;AAAA,MACF;AACA,oBAAc;AAAA,IAChB;AAEA,UAAM,kBACJ,oBAAoB,WAAW,KAAK;AACtC,QAAI;AAEJ,QAAI;AACF,UAAI,YAAY,eAAe,GAAG;AAChC,eAAO,KAAK,MAAM,eAAe;AAAA,MACnC,OAAO;AACL,cAAM,SAAS,cAAc,eAAe;AAC5C,eAAO,KAAK,MAAM,MAAM;AACxB,YAAI,CAAC,aAAa;AAChB,mBAAS,KAAK,iDAAiD;AAAA,QACjE;AAAA,MACF;AAAA,IACF,SAAS,YAAY;AACnB,YAAM,WAAW,uBAA0B,eAAe;AAC1D,UAAI,UAAU;AACZ,eAAO;AACP,sBAAc;AACd,iBAAS,KAAK,yCAAyC;AAAA,MACzD,OAAO;AACL,cAAM,IAAI;AAAA,UACR,kCAAkC,iBAAiB,mBAAmB,UAAU;AAAA,QAClF;AAAA,MACF;AAAA,IACF;AAEA,QAAI,QAAQ;AACV,UAAI;AACF,eAAO,OAAO,MAAM,IAAI;AAAA,MAC1B,SAAS,iBAAiB;AACxB,gBAAQ;AAAA,UACN;AAAA,UACA;AAAA,QACF;AACA,iBAAS,KAAK,4BAA4B,eAAe,EAAE;AAAA,MAC7D;AAAA,IACF;AAEA,YAAQ;AAAA,MACN,2CAA2C,iBAAiB,eAAe,SAAS,MAAM;AAAA,IAC5F;AAEA,WAAO;AAAA,MACL;AAAA,MACA,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,YAAQ,MAAM,yCAAyC,KAAK;AAC5D,UAAM;AAAA,EACR;AACF;AAcO,SAAS,+BACd,SACA,iBACA,SACA,cAAsB,2BACd;AACR,SAAO,oCAAoC,OAAO,IAAI,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,EAKjE,OAAO;AAAA;AAAA;AAAA;AAAA,EAIP,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUjB;","names":["DEFAULT_MODELS","DEFAULT_BASE_URLS","API_KEY_ENV_VARS","bracketInfo"]}
1
+ {"version":3,"sources":["../src/embedding-client.ts","../src/image-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 * - `lmstudio` (default model `text-embedding-bge-base-en-v1.5`, same\n * bge-base-en-v1.5 weights as the openrouter default, run locally) —\n * local LM Studio server, no API key. Mirrors `client.ts`'s\n * `PROVIDER_CONFIGS.lmstudio`: base URL from `LMSTUDIO_URL` (default\n * `http://127.0.0.1:1234/v1`), model from `LMSTUDIO_EMBEDDING_MODEL`.\n *\n * All three 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' | 'lmstudio';\n\nconst EMBEDDING_PROVIDERS: readonly EmbeddingProvider[] = ['openai', 'openrouter', 'lmstudio'];\n\n/**\n * Canonical embedding-provider resolution — the ONE place every downstream\n * consumer should defer to instead of hardcoding a literal provider.\n * Mirrors the `EMBEDDING_PROVIDER` env var `@almadar-io/integrations`'s\n * `RuntimeIntegrationManager`/`getEmbeddings()` already reads (its own\n * `?? 'openrouter'` fallback is the precedent this centralizes).\n *\n * Defaults to `openrouter` — UNCHANGED from every existing deployment's\n * behavior — so nothing (cloud/server included) switches to a local-only\n * provider unless an operator explicitly sets `EMBEDDING_PROVIDER=lmstudio`\n * in that specific environment (e.g. a dev machine running LM Studio).\n * Consumers: `@almadar-io/rabit`'s catalog narrower\n * (`runtime/services/embedding.ts`) and `@almadar/workspace`'s index\n * embedder (`workspace-index/embedder.ts`).\n */\nexport function resolveDefaultEmbeddingProvider(): EmbeddingProvider {\n const v = process.env.EMBEDDING_PROVIDER;\n return v && (EMBEDDING_PROVIDERS as readonly string[]).includes(v) ? (v as EmbeddingProvider) : 'openrouter';\n}\n\nexport interface EmbeddingClientOptions {\n provider: EmbeddingProvider;\n /** Defaults: openai → text-embedding-3-small, openrouter → baai/bge-base-en-v1.5, lmstudio → text-embedding-bge-base-en-v1.5. */\n model?: string;\n /** Override API key. Defaults to provider-specific env var; lmstudio needs none. */\n apiKey?: string;\n /** Override base URL. Defaults to provider canonical endpoint (lmstudio: `LMSTUDIO_URL`). */\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 lmstudio: 'text-embedding-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 lmstudio: 'http://127.0.0.1:1234/v1',\n};\n\nconst API_KEY_ENV_VARS: Partial<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 ?? (this.provider === 'lmstudio' ? process.env.LMSTUDIO_EMBEDDING_MODEL ?? DEFAULT_MODELS.lmstudio : DEFAULT_MODELS[this.provider]);\n const defaultBaseUrl = this.provider === 'lmstudio' ? process.env.LMSTUDIO_URL ?? DEFAULT_BASE_URLS.lmstudio : DEFAULT_BASE_URLS[this.provider];\n this.baseUrl = (opts.baseUrl ?? defaultBaseUrl).replace(/\\/$/, '');\n\n if (this.provider === 'lmstudio') {\n // Local LM Studio server — no credential to check, mirrors client.ts's PROVIDER_CONFIGS.lmstudio.\n this.apiKey = opts.apiKey ?? 'not-needed';\n return;\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 const envVar = API_KEY_ENV_VARS[this.provider];\n return envVar ? process.env[envVar] : undefined;\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 * Image Client\n *\n * Single-purpose client for image-generation APIs. `openrouter` posts to\n * `POST {baseUrl}/images`; `local` posts an OpenAI-shaped body to\n * `POST {baseUrl}/images/generations` (the MFLUX shim in\n * tools/asset-workflow/python/image_server). Raw `fetch`, no SDK — mirrors\n * `embedding-client.ts`'s shape (provider tables, retry policy, the\n * three response-error surfaces).\n *\n * @packageDocumentation\n */\n\nimport type { JsonValue } from '@almadar/core';\nimport { Agent as UndiciAgent, fetch as undiciFetch, type RequestInit as UndiciRequestInit, type Response as UndiciResponse } from 'undici';\n\nexport type ImageProvider = 'openrouter' | 'local';\n\nconst IMAGE_PROVIDERS: readonly ImageProvider[] = ['openrouter', 'local'];\n\n/** Canonical image-provider resolution, mirrors `resolveDefaultEmbeddingProvider`. */\nexport function resolveDefaultImageProvider(): ImageProvider {\n const v = process.env.IMAGE_PROVIDER;\n return v && (IMAGE_PROVIDERS as readonly string[]).includes(v) ? (v as ImageProvider) : 'openrouter';\n}\n\nexport const OPENROUTER_IMAGE_MODELS = {\n SEEDREAM_5_LITE: 'bytedance-seed/seedream-5-0-lite',\n SEEDREAM_4_5: 'bytedance-seed/seedream-4.5',\n GEMINI_3_1_FLASH_LITE_IMAGE: 'google/gemini-3.1-flash-lite-image',\n GEMINI_2_5_FLASH_IMAGE: 'google/gemini-2.5-flash-image',\n FLUX_2_PRO: 'black-forest-labs/flux.2-pro',\n} as const;\n\nexport const OPENROUTER_IMAGE_DEFAULTS = {\n sheet: OPENROUTER_IMAGE_MODELS.SEEDREAM_5_LITE,\n preview: OPENROUTER_IMAGE_MODELS.GEMINI_3_1_FLASH_LITE_IMAGE,\n} as const;\n\nexport const LOCAL_IMAGE_MODELS = {\n Z_IMAGE_TURBO: 'z-image-turbo',\n FLUX_SCHNELL: 'flux-schnell',\n FLUX_DEV: 'flux-dev',\n FLUX_KONTEXT: 'flux-kontext',\n FLUX2_KLEIN: 'flux2-klein',\n} as const;\n\nexport const LOCAL_IMAGE_DEFAULTS = {\n /** Z-Image-Turbo is the default: public on Hugging Face, no token required (FLUX is gated). */\n sheet: process.env.LOCAL_IMAGE_SHEET_MODEL ?? LOCAL_IMAGE_MODELS.Z_IMAGE_TURBO,\n preview: process.env.LOCAL_IMAGE_PREVIEW_MODEL ?? LOCAL_IMAGE_MODELS.Z_IMAGE_TURBO,\n};\n\nexport interface ImageClientOptions {\n provider?: ImageProvider;\n model?: string;\n apiKey?: string;\n baseUrl?: string;\n fetchImpl?: typeof fetch;\n}\n\n/** An input reference image — `https:` or `data:` URL. Transport only, no fetching/validation. */\nexport interface ImageReference {\n url: string;\n}\n\nexport interface ImageGenerateOptions {\n prompt: string;\n model?: string;\n n?: number;\n size?: `${number}x${number}`;\n resolution?: '512' | '1K' | '2K' | '4K';\n aspectRatio?: string;\n quality?: 'auto' | 'low' | 'medium' | 'high';\n background?: 'auto' | 'transparent' | 'opaque';\n seed?: number;\n /** Diffusion step count (local shim; 1–100). */\n steps?: number;\n /** img2img denoise strength 0–1 (local shim `image_strength`), used with `references`. */\n strength?: number;\n references?: ReadonlyArray<ImageReference>;\n provider?: { order?: string[]; only?: string[]; allowFallbacks?: boolean };\n}\n\nexport interface GeneratedImage {\n bytes: Uint8Array;\n mediaType: string;\n}\n\nexport interface ImageGenerateResult {\n images: GeneratedImage[];\n usage: { cost: number; totalTokens: number };\n model: string;\n dropped: string[];\n}\n\nexport interface ImageModelCapabilities {\n model: string;\n supportedParameters: ReadonlySet<string>;\n raw: JsonValue;\n}\n\n/** Transient-failure retry policy: attempts and backoff (ms) between them. */\nconst IMAGE_MAX_ATTEMPTS = 3;\nconst IMAGE_RETRY_BACKOFF_MS = [500, 2000];\n\nconst DEFAULT_MODELS: Record<ImageProvider, string> = {\n openrouter: OPENROUTER_IMAGE_DEFAULTS.sheet,\n local: LOCAL_IMAGE_DEFAULTS.sheet,\n};\n\nconst DEFAULT_BASE_URLS: Record<ImageProvider, string> = {\n openrouter: 'https://openrouter.ai/api/v1',\n local: 'http://127.0.0.1:8180/v1',\n};\n\nconst API_KEY_ENV_VARS: Record<ImageProvider, string> = {\n openrouter: 'OPEN_ROUTER_API_KEY',\n local: 'LOCAL_IMAGE_API_KEY',\n};\n\n/**\n * Request field → OpenRouter `supported_parameters` name. The single\n * place `generateChecked` reads to decide what to strip — change the\n * mapping here, not at each call site.\n */\nconst IMAGE_CHECKED_PARAM_NAMES = {\n background: 'background',\n seed: 'seed',\n steps: 'steps',\n strength: 'image_strength',\n references: 'input_references',\n} as const;\n\nfunction isJsonObject(value: JsonValue): value is { [key: string]: JsonValue } {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction asString(value: JsonValue | undefined): string | undefined {\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction asNumber(value: JsonValue | undefined): number | undefined {\n return typeof value === 'number' ? value : undefined;\n}\n\nfunction parseJsonBody(provider: ImageProvider, rawBody: string): JsonValue {\n try {\n return JSON.parse(rawBody) as JsonValue;\n } catch {\n throw new Error(`ImageClient: ${provider} returned unparseable body: ${rawBody}`);\n }\n}\n\nfunction decodeBase64(b64: string): Uint8Array {\n if (typeof globalThis.Buffer !== 'undefined') {\n return new Uint8Array(globalThis.Buffer.from(b64, 'base64'));\n }\n const binary = atob(b64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes;\n}\n\n/**\n * Collects the union of `supported_parameters` across a `.../endpoints`\n * response's `data`/`endpoints` arrays. Returns an empty set (never\n * throws) when the shape doesn't match — the caller still gets `raw`.\n */\nfunction collectSupportedParameters(body: JsonValue): ReadonlySet<string> {\n const params = new Set<string>();\n if (!isJsonObject(body)) return params;\n const candidateArrays: JsonValue[][] = [];\n if (Array.isArray(body.data)) candidateArrays.push(body.data);\n if (Array.isArray(body.endpoints)) candidateArrays.push(body.endpoints);\n for (const arr of candidateArrays) {\n for (const entry of arr) {\n if (!isJsonObject(entry)) continue;\n const sp = entry.supported_parameters;\n if (!Array.isArray(sp)) continue;\n for (const p of sp) if (typeof p === 'string') params.add(p);\n }\n }\n return params;\n}\n\nexport class ImageClient {\n private readonly provider: ImageProvider;\n private readonly model: string;\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n private readonly capabilitiesCache = new Map<string, ImageModelCapabilities>();\n\n constructor(opts: ImageClientOptions = {}) {\n this.provider = opts.provider ?? resolveDefaultImageProvider();\n this.model = opts.model ?? DEFAULT_MODELS[this.provider];\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URLS[this.provider]).replace(/\\/$/, '');\n this.fetchImpl = opts.fetchImpl ?? fetch;\n\n // The local shim is unauthenticated; a key is required only for hosted providers.\n const apiKey = opts.apiKey ?? process.env[API_KEY_ENV_VARS[this.provider]] ?? '';\n if (!apiKey && this.provider !== 'local') {\n throw new Error(\n `ImageClient: 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 /** Generate images. Never strips request fields — `dropped` is always `[]`. */\n async generate(opts: ImageGenerateOptions): Promise<ImageGenerateResult> {\n return this.performGenerate(opts, []);\n }\n\n /**\n * Per-model `.../endpoints` capabilities, memoized per instance per\n * model. `supportedParameters` is the empty set (not a throw) when the\n * response shape doesn't match what's expected.\n */\n async capabilities(modelId?: string): Promise<ImageModelCapabilities> {\n const model = modelId ?? this.model;\n const cached = this.capabilitiesCache.get(model);\n if (cached) return cached;\n\n // The local shim serves no `.../endpoints` metadata; declare what it honors.\n if (this.provider === 'local') {\n const local: ImageModelCapabilities = {\n model,\n supportedParameters: new Set(['seed', IMAGE_CHECKED_PARAM_NAMES.steps, IMAGE_CHECKED_PARAM_NAMES.strength, IMAGE_CHECKED_PARAM_NAMES.references]),\n raw: {},\n };\n this.capabilitiesCache.set(model, local);\n return local;\n }\n\n const response = await this.fetchWithRetry(`${this.baseUrl}/images/models/${model}/endpoints`, {\n method: 'GET',\n headers: this.authHeaders(),\n });\n if (!response.ok) {\n const errorText = await response.text().catch(() => '<no body>');\n throw new Error(\n `ImageClient: ${this.provider} returned ${response.status} ${response.statusText}: ${errorText}`,\n );\n }\n const body = parseJsonBody(this.provider, await response.text());\n const result: ImageModelCapabilities = { model, supportedParameters: collectSupportedParameters(body), raw: body };\n this.capabilitiesCache.set(model, result);\n return result;\n }\n\n /**\n * Like `generate`, but first reads `capabilities(model)` and strips\n * any of `background`/`seed`/`references` the model doesn't list in\n * `supported_parameters` — their (OpenRouter) names come back in\n * `dropped` rather than being silently swallowed.\n */\n async generateChecked(opts: ImageGenerateOptions): Promise<ImageGenerateResult> {\n const model = opts.model ?? this.model;\n const caps = await this.capabilities(model);\n const dropped: string[] = [];\n const filtered: ImageGenerateOptions = { ...opts, model };\n\n if (filtered.background !== undefined && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.background)) {\n delete filtered.background;\n dropped.push(IMAGE_CHECKED_PARAM_NAMES.background);\n }\n if (filtered.seed !== undefined && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.seed)) {\n delete filtered.seed;\n dropped.push(IMAGE_CHECKED_PARAM_NAMES.seed);\n }\n if (filtered.steps !== undefined && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.steps)) {\n delete filtered.steps;\n dropped.push(IMAGE_CHECKED_PARAM_NAMES.steps);\n }\n if (filtered.strength !== undefined && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.strength)) {\n delete filtered.strength;\n dropped.push(IMAGE_CHECKED_PARAM_NAMES.strength);\n }\n if (\n filtered.references !== undefined &&\n filtered.references.length > 0 &&\n !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.references)\n ) {\n delete filtered.references;\n dropped.push(IMAGE_CHECKED_PARAM_NAMES.references);\n }\n\n return this.performGenerate(filtered, dropped);\n }\n\n private async performGenerate(opts: ImageGenerateOptions, dropped: string[]): Promise<ImageGenerateResult> {\n if (opts.size !== undefined && (opts.resolution !== undefined || opts.aspectRatio !== undefined)) {\n throw new Error('ImageClient.generate: `size` cannot be combined with `resolution`/`aspectRatio` — pass one or the other');\n }\n const model = opts.model ?? this.model;\n\n if (this.provider === 'local') {\n return this.performLocalGenerate(opts, model, dropped);\n }\n\n const requestBody: Record<string, JsonValue> = { model, prompt: opts.prompt, output_format: 'png' };\n if (opts.n !== undefined) requestBody.n = opts.n;\n if (opts.size !== undefined) requestBody.size = opts.size;\n if (opts.resolution !== undefined) requestBody.resolution = opts.resolution;\n if (opts.aspectRatio !== undefined) requestBody.aspect_ratio = opts.aspectRatio;\n if (opts.quality !== undefined) requestBody.quality = opts.quality;\n if (opts.background !== undefined) requestBody.background = opts.background;\n if (opts.seed !== undefined) requestBody.seed = opts.seed;\n if (opts.steps !== undefined) requestBody.steps = opts.steps;\n if (opts.references !== undefined && opts.references.length > 0) {\n requestBody.input_references = opts.references.map((r) => ({ type: 'image_url', image_url: { url: r.url } }));\n }\n if (opts.provider !== undefined) {\n const providerField: Record<string, JsonValue> = {};\n if (opts.provider.order !== undefined) providerField.order = opts.provider.order;\n if (opts.provider.only !== undefined) providerField.only = opts.provider.only;\n if (opts.provider.allowFallbacks !== undefined) providerField.allow_fallbacks = opts.provider.allowFallbacks;\n requestBody.provider = providerField;\n }\n\n return this.sendGenerate(`${this.baseUrl}/images`, requestBody, model, dropped);\n }\n\n /**\n * OpenAI-compatible local server (the MFLUX shim in tools/asset-workflow).\n * OpenRouter-only fields are reported in `dropped`, never sent. References\n * ARE sent (as `input_references`) — the shim forwards them to mflux's\n * `image_path`/`image_strength` img2img conditioning.\n */\n private async performLocalGenerate(\n opts: ImageGenerateOptions,\n model: string,\n dropped: string[],\n ): Promise<ImageGenerateResult> {\n const requestBody: Record<string, JsonValue> = { model, prompt: opts.prompt };\n if (opts.n !== undefined) requestBody.n = opts.n;\n if (opts.size !== undefined) requestBody.size = opts.size;\n if (opts.seed !== undefined) requestBody.seed = opts.seed;\n if (opts.steps !== undefined) requestBody.steps = opts.steps;\n if (opts.strength !== undefined) requestBody.image_strength = opts.strength;\n if (opts.references !== undefined && opts.references.length > 0) {\n requestBody.input_references = opts.references.map((r) => ({ type: 'image_url', image_url: { url: r.url } }));\n }\n for (const field of ['resolution', 'aspectRatio', 'quality', 'background'] as const) {\n if (opts[field] !== undefined) dropped.push(field);\n }\n if (opts.provider !== undefined) dropped.push('provider');\n\n return this.sendGenerate(`${this.baseUrl}/images/generations`, requestBody, model, dropped);\n }\n\n private authHeaders(): Record<string, string> {\n return this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {};\n }\n\n private async sendGenerate(\n url: string,\n requestBody: Record<string, JsonValue>,\n model: string,\n dropped: string[],\n ): Promise<ImageGenerateResult> {\n const response = await this.fetchWithRetry(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...this.authHeaders() },\n body: JSON.stringify(requestBody),\n });\n if (!response.ok) {\n const errorText = await response.text().catch(() => '<no body>');\n throw new Error(\n `ImageClient: ${this.provider} returned ${response.status} ${response.statusText}: ${errorText}`,\n );\n }\n const rawBody = await response.text();\n const body = parseJsonBody(this.provider, rawBody);\n if (!isJsonObject(body)) {\n throw new Error(`ImageClient: ${this.provider} returned unparseable body: ${rawBody}`);\n }\n if (isJsonObject(body.error)) {\n const detail = asString(body.error.message) ?? rawBody;\n throw new Error(`ImageClient: ${this.provider} returned an error: ${detail}`);\n }\n if (!Array.isArray(body.data)) {\n throw new Error(`ImageClient: ${this.provider} response has no \\`data\\` array: ${rawBody}`);\n }\n\n const images: GeneratedImage[] = body.data.map((entry, i) => {\n if (!isJsonObject(entry)) {\n throw new Error(`ImageClient: ${this.provider} response data[${i}] is not an object: ${rawBody}`);\n }\n const b64 = asString(entry.b64_json);\n if (!b64) {\n throw new Error(`ImageClient: ${this.provider} response data[${i}] missing \\`b64_json\\`: ${rawBody}`);\n }\n return { bytes: decodeBase64(b64), mediaType: asString(entry.media_type) ?? 'image/png' };\n });\n\n const usageObj = isJsonObject(body.usage) ? body.usage : undefined;\n const usage = {\n cost: asNumber(usageObj?.cost) ?? 0,\n totalTokens: asNumber(usageObj?.total_tokens) ?? 0,\n };\n\n return { images, usage, model, dropped };\n }\n\n /** Retries on network failure or a 5xx status; any other response is returned as-is.\n * The local provider never retries: a failed local generation (e.g. MLX OOM) cannot\n * succeed on an immediate retry, and hammering a memory-pressured machine is harmful. */\n private async fetchWithRetry(url: string, init: RequestInit): Promise<Response | UndiciResponse> {\n const maxAttempts = this.provider === 'local' ? 1 : IMAGE_MAX_ATTEMPTS;\n let lastNetworkError: Error | undefined;\n let response: Response | UndiciResponse | undefined;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n response = await this.doFetch(url, init);\n } catch (e) {\n response = undefined;\n lastNetworkError = e instanceof Error ? e : new Error(String(e));\n }\n if (response !== undefined && response.status < 500) return response;\n const backoff = IMAGE_RETRY_BACKOFF_MS[attempt - 1];\n if (backoff === undefined || attempt >= maxAttempts) break;\n await new Promise((r) => setTimeout(r, backoff));\n }\n if (response) return response;\n throw lastNetworkError ?? new Error(`ImageClient: ${this.provider} request failed after ${maxAttempts} attempts`);\n }\n\n /** Local generations can exceed Node's built-in 300s headersTimeout (a single\n * MFLUX image routinely takes minutes), so local calls ride npm-undici's own\n * fetch with the transport timers disabled — mirrors `LLMClient.localDispatcher`. */\n private static localDispatcher: UndiciAgent | null = null;\n\n private static getLocalDispatcher(): UndiciAgent {\n if (ImageClient.localDispatcher === null) {\n ImageClient.localDispatcher = new UndiciAgent({ headersTimeout: 0, bodyTimeout: 0 });\n }\n return ImageClient.localDispatcher;\n }\n\n private async doFetch(url: string, init: RequestInit): Promise<Response | UndiciResponse> {\n if (this.provider === 'local' && this.fetchImpl === fetch) {\n const uinit: UndiciRequestInit = {\n method: init.method,\n headers: init.headers as UndiciRequestInit['headers'],\n body: init.body as UndiciRequestInit['body'],\n dispatcher: ImageClient.getLocalDispatcher(),\n };\n return undiciFetch(url, uinit);\n }\n return this.fetchImpl(url, init);\n }\n}\n\nexport const IMAGE_PROVIDER_CATALOG: ReadonlyArray<{ provider: ImageProvider; models: string[]; defaultModel: string }> = [\n {\n provider: 'openrouter',\n models: Object.values(OPENROUTER_IMAGE_MODELS),\n defaultModel: OPENROUTER_IMAGE_DEFAULTS.sheet,\n },\n {\n provider: 'local',\n models: Object.values(LOCAL_IMAGE_MODELS),\n defaultModel: LOCAL_IMAGE_DEFAULTS.sheet,\n },\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,IAAM,sBAAoD,CAAC,UAAU,cAAc,UAAU;AAiBtF,SAAS,kCAAqD;AACnE,QAAM,IAAI,QAAQ,IAAI;AACtB,SAAO,KAAM,oBAA0C,SAAS,CAAC,IAAK,IAA0B;AAClG;AAmCA,IAAM,qBAAqB;AAC3B,IAAM,yBAAyB,CAAC,KAAK,GAAI;AAGzC,IAAM,oBACJ;AAEF,IAAM,iBAAoD;AAAA,EACxD,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,UAAU;AACZ;AAEA,IAAM,oBAAuD;AAAA,EAC3D,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,UAAU;AACZ;AAEA,IAAM,mBAA+D;AAAA,EACnE,QAAQ;AAAA,EACR,YAAY;AACd;AAEO,IAAM,kBAAN,MAAsB;AAAA,EAM3B,YAAY,MAA8B;AACxC,SAAK,WAAW,KAAK;AACrB,SAAK,QAAQ,KAAK,UAAU,KAAK,aAAa,aAAa,QAAQ,IAAI,4BAA4B,eAAe,WAAW,eAAe,KAAK,QAAQ;AACzJ,UAAM,iBAAiB,KAAK,aAAa,aAAa,QAAQ,IAAI,gBAAgB,kBAAkB,WAAW,kBAAkB,KAAK,QAAQ;AAC9I,SAAK,WAAW,KAAK,WAAW,gBAAgB,QAAQ,OAAO,EAAE;AAEjE,QAAI,KAAK,aAAa,YAAY;AAEhC,WAAK,SAAS,KAAK,UAAU;AAC7B;AAAA,IACF;AACA,UAAM,SAAS,KAAK,UAAU,KAAK,cAAc;AACjD,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI;AAAA,QACR,0CAA0C,KAAK,QAAQ,oBAC9C,iBAAiB,KAAK,QAAQ,CAAC;AAAA,MAC1C;AAAA,IACF;AACA,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA,EAGA,MAAM,MAAM,MAAwC;AAClD,UAAM,QAAQ,MAAM,KAAK,WAAW,CAAC,IAAI,CAAC;AAC1C,UAAM,YAAY,MAAM,WAAW,CAAC;AACpC,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,MAAM,uDAAuD;AAAA,IACzE;AACA,WAAO,EAAE,WAAW,OAAO,MAAM,MAAM;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,WAAW,OAA6D;AAC5E,QAAI,MAAM,WAAW,GAAG;AACtB,aAAO,EAAE,YAAY,CAAC,GAAG,OAAO,EAAE,cAAc,GAAG,aAAa,EAAE,EAAE;AAAA,IACtE;AAOA,QAAI,UAAU;AACd,QAAI,YAAmB,IAAI,MAAM,8BAA8B;AAC/D,aAAS,UAAU,GAAG,WAAW,oBAAoB,WAAW;AAC9D,YAAM,UAAU,MAAM,IAAI,CAAC,MAAO,EAAE,SAAS,UAAU,EAAE,MAAM,GAAG,OAAO,IAAI,CAAE;AAC/E,UAAI;AACF,eAAO,MAAM,KAAK,eAAe,MAAM,QAAQ,OAAO;AAAA,MACxD,SAAS,GAAG;AACV,oBAAY,aAAa,QAAQ,IAAI,IAAI,MAAM,OAAO,CAAC,CAAC;AACxD,YAAI,kBAAkB,KAAK,UAAU,OAAO,KAAK,UAAU,KAAK;AAC9D,oBAAU,KAAK,MAAM,UAAU,CAAC;AAChC;AAAA,QACF;AACA,cAAM,UAAU,uBAAuB,UAAU,CAAC;AAClD,YAAI,YAAY,OAAW;AAC3B,cAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC;AAAA,MACjD;AAAA,IACF;AACA,UAAM;AAAA,EACR;AAAA,EAEA,MAAc,eACZ,YACA,SAC+B;AAC/B,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,eAAe;AAAA,MACzD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,eAAe,UAAU,KAAK,MAAM;AAAA,MACtC;AAAA,MACA,MAAM,KAAK,UAAU,EAAE,OAAO,KAAK,OAAO,OAAO,QAAQ,CAAC;AAAA,IAC5D,CAAC;AACD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,WAAW;AAC/D,YAAM,IAAI;AAAA,QACR,oBAAoB,KAAK,QAAQ,aAAa,SAAS,MAAM,IAAI,SAAS,UAAU,KAAK,SAAS;AAAA,MACpG;AAAA,IACF;AACA,UAAM,UAAU,MAAM,SAAS,KAAK;AACpC,QAAI;AACJ,QAAI;AACF,aAAO,KAAK,MAAM,OAAO;AAAA,IAC3B,QAAQ;AACN,YAAM,IAAI;AAAA,QACR,oBAAoB,KAAK,QAAQ,+BAA+B,OAAO;AAAA,MACzE;AAAA,IACF;AACA,QAAI,CAAC,MAAM,QAAQ,KAAK,IAAI,GAAG;AAI7B,YAAM,SAAS,KAAK,OAAO,WAAW;AACtC,YAAM,IAAI;AAAA,QACR,oBAAoB,KAAK,QAAQ,oCAAoC,MAAM;AAAA,MAC7E;AAAA,IACF;AACA,UAAM,UAAU,CAAC,GAAG,KAAK,IAAI,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAC/D,QAAI,QAAQ,WAAW,YAAY;AACjC,YAAM,IAAI;AAAA,QACR,sCAAsC,QAAQ,MAAM,mBAAmB,UAAU;AAAA,MACnF;AAAA,IACF;AACA,WAAO;AAAA,MACL,YAAY,QAAQ,IAAI,CAAC,MAAM,EAAE,SAAS;AAAA,MAC1C,OAAO;AAAA,QACL,cAAc,KAAK,OAAO,iBAAiB;AAAA,QAC3C,aAAa,KAAK,OAAO,gBAAgB;AAAA,MAC3C;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,gBAAoC;AAC1C,UAAM,SAAS,iBAAiB,KAAK,QAAQ;AAC7C,WAAO,SAAS,QAAQ,IAAI,MAAM,IAAI;AAAA,EACxC;AACF;AAWO,SAAS,iBAAiB,GAA0B,GAAkC;AAC3F,MAAI,EAAE,WAAW,EAAE,QAAQ;AACzB,UAAM,IAAI,MAAM,6CAA6C,EAAE,MAAM,OAAO,EAAE,MAAM,GAAG;AAAA,EACzF;AACA,MAAI,MAAM;AACV,MAAI,QAAQ;AACZ,MAAI,QAAQ;AACZ,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,UAAM,KAAK,EAAE,CAAC;AACd,UAAM,KAAK,EAAE,CAAC;AACd,WAAO,KAAK;AACZ,aAAS,KAAK;AACd,aAAS,KAAK;AAAA,EAChB;AACA,MAAI,UAAU,KAAK,UAAU,EAAG,QAAO;AACvC,SAAO,OAAO,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK;AAClD;;;ACvPA,SAAS,SAAS,aAAa,SAAS,mBAA2F;AAInI,IAAM,kBAA4C,CAAC,cAAc,OAAO;AAGjE,SAAS,8BAA6C;AAC3D,QAAM,IAAI,QAAQ,IAAI;AACtB,SAAO,KAAM,gBAAsC,SAAS,CAAC,IAAK,IAAsB;AAC1F;AAEO,IAAM,0BAA0B;AAAA,EACrC,iBAAiB;AAAA,EACjB,cAAc;AAAA,EACd,6BAA6B;AAAA,EAC7B,wBAAwB;AAAA,EACxB,YAAY;AACd;AAEO,IAAM,4BAA4B;AAAA,EACvC,OAAO,wBAAwB;AAAA,EAC/B,SAAS,wBAAwB;AACnC;AAEO,IAAM,qBAAqB;AAAA,EAChC,eAAe;AAAA,EACf,cAAc;AAAA,EACd,UAAU;AAAA,EACV,cAAc;AAAA,EACd,aAAa;AACf;AAEO,IAAM,uBAAuB;AAAA;AAAA,EAElC,OAAO,QAAQ,IAAI,2BAA2B,mBAAmB;AAAA,EACjE,SAAS,QAAQ,IAAI,6BAA6B,mBAAmB;AACvE;AAoDA,IAAM,qBAAqB;AAC3B,IAAM,yBAAyB,CAAC,KAAK,GAAI;AAEzC,IAAMA,kBAAgD;AAAA,EACpD,YAAY,0BAA0B;AAAA,EACtC,OAAO,qBAAqB;AAC9B;AAEA,IAAMC,qBAAmD;AAAA,EACvD,YAAY;AAAA,EACZ,OAAO;AACT;AAEA,IAAMC,oBAAkD;AAAA,EACtD,YAAY;AAAA,EACZ,OAAO;AACT;AAOA,IAAM,4BAA4B;AAAA,EAChC,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,OAAO;AAAA,EACP,UAAU;AAAA,EACV,YAAY;AACd;AAEA,SAAS,aAAa,OAAyD;AAC7E,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;AAEA,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;AAEA,SAAS,cAAc,UAAyB,SAA4B;AAC1E,MAAI;AACF,WAAO,KAAK,MAAM,OAAO;AAAA,EAC3B,QAAQ;AACN,UAAM,IAAI,MAAM,gBAAgB,QAAQ,+BAA+B,OAAO,EAAE;AAAA,EAClF;AACF;AAEA,SAAS,aAAa,KAAyB;AAC7C,MAAI,OAAO,WAAW,WAAW,aAAa;AAC5C,WAAO,IAAI,WAAW,WAAW,OAAO,KAAK,KAAK,QAAQ,CAAC;AAAA,EAC7D;AACA,QAAM,SAAS,KAAK,GAAG;AACvB,QAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;AAC1C,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,IAAK,OAAM,CAAC,IAAI,OAAO,WAAW,CAAC;AACtE,SAAO;AACT;AAOA,SAAS,2BAA2B,MAAsC;AACxE,QAAM,SAAS,oBAAI,IAAY;AAC/B,MAAI,CAAC,aAAa,IAAI,EAAG,QAAO;AAChC,QAAM,kBAAiC,CAAC;AACxC,MAAI,MAAM,QAAQ,KAAK,IAAI,EAAG,iBAAgB,KAAK,KAAK,IAAI;AAC5D,MAAI,MAAM,QAAQ,KAAK,SAAS,EAAG,iBAAgB,KAAK,KAAK,SAAS;AACtE,aAAW,OAAO,iBAAiB;AACjC,eAAW,SAAS,KAAK;AACvB,UAAI,CAAC,aAAa,KAAK,EAAG;AAC1B,YAAM,KAAK,MAAM;AACjB,UAAI,CAAC,MAAM,QAAQ,EAAE,EAAG;AACxB,iBAAW,KAAK,GAAI,KAAI,OAAO,MAAM,SAAU,QAAO,IAAI,CAAC;AAAA,IAC7D;AAAA,EACF;AACA,SAAO;AACT;AAEO,IAAM,eAAN,MAAM,aAAY;AAAA,EAQvB,YAAY,OAA2B,CAAC,GAAG;AAF3C,SAAiB,oBAAoB,oBAAI,IAAoC;AAG3E,SAAK,WAAW,KAAK,YAAY,4BAA4B;AAC7D,SAAK,QAAQ,KAAK,SAASF,gBAAe,KAAK,QAAQ;AACvD,SAAK,WAAW,KAAK,WAAWC,mBAAkB,KAAK,QAAQ,GAAG,QAAQ,OAAO,EAAE;AACnF,SAAK,YAAY,KAAK,aAAa;AAGnC,UAAM,SAAS,KAAK,UAAU,QAAQ,IAAIC,kBAAiB,KAAK,QAAQ,CAAC,KAAK;AAC9E,QAAI,CAAC,UAAU,KAAK,aAAa,SAAS;AACxC,YAAM,IAAI;AAAA,QACR,sCAAsC,KAAK,QAAQ,oBAC1CA,kBAAiB,KAAK,QAAQ,CAAC;AAAA,MAC1C;AAAA,IACF;AACA,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA,EAGA,MAAM,SAAS,MAA0D;AACvE,WAAO,KAAK,gBAAgB,MAAM,CAAC,CAAC;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,aAAa,SAAmD;AACpE,UAAM,QAAQ,WAAW,KAAK;AAC9B,UAAM,SAAS,KAAK,kBAAkB,IAAI,KAAK;AAC/C,QAAI,OAAQ,QAAO;AAGnB,QAAI,KAAK,aAAa,SAAS;AAC7B,YAAM,QAAgC;AAAA,QACpC;AAAA,QACA,qBAAqB,oBAAI,IAAI,CAAC,QAAQ,0BAA0B,OAAO,0BAA0B,UAAU,0BAA0B,UAAU,CAAC;AAAA,QAChJ,KAAK,CAAC;AAAA,MACR;AACA,WAAK,kBAAkB,IAAI,OAAO,KAAK;AACvC,aAAO;AAAA,IACT;AAEA,UAAM,WAAW,MAAM,KAAK,eAAe,GAAG,KAAK,OAAO,kBAAkB,KAAK,cAAc;AAAA,MAC7F,QAAQ;AAAA,MACR,SAAS,KAAK,YAAY;AAAA,IAC5B,CAAC;AACD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,WAAW;AAC/D,YAAM,IAAI;AAAA,QACR,gBAAgB,KAAK,QAAQ,aAAa,SAAS,MAAM,IAAI,SAAS,UAAU,KAAK,SAAS;AAAA,MAChG;AAAA,IACF;AACA,UAAM,OAAO,cAAc,KAAK,UAAU,MAAM,SAAS,KAAK,CAAC;AAC/D,UAAM,SAAiC,EAAE,OAAO,qBAAqB,2BAA2B,IAAI,GAAG,KAAK,KAAK;AACjH,SAAK,kBAAkB,IAAI,OAAO,MAAM;AACxC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,gBAAgB,MAA0D;AAC9E,UAAM,QAAQ,KAAK,SAAS,KAAK;AACjC,UAAM,OAAO,MAAM,KAAK,aAAa,KAAK;AAC1C,UAAM,UAAoB,CAAC;AAC3B,UAAM,WAAiC,EAAE,GAAG,MAAM,MAAM;AAExD,QAAI,SAAS,eAAe,UAAa,CAAC,KAAK,oBAAoB,IAAI,0BAA0B,UAAU,GAAG;AAC5G,aAAO,SAAS;AAChB,cAAQ,KAAK,0BAA0B,UAAU;AAAA,IACnD;AACA,QAAI,SAAS,SAAS,UAAa,CAAC,KAAK,oBAAoB,IAAI,0BAA0B,IAAI,GAAG;AAChG,aAAO,SAAS;AAChB,cAAQ,KAAK,0BAA0B,IAAI;AAAA,IAC7C;AACA,QAAI,SAAS,UAAU,UAAa,CAAC,KAAK,oBAAoB,IAAI,0BAA0B,KAAK,GAAG;AAClG,aAAO,SAAS;AAChB,cAAQ,KAAK,0BAA0B,KAAK;AAAA,IAC9C;AACA,QAAI,SAAS,aAAa,UAAa,CAAC,KAAK,oBAAoB,IAAI,0BAA0B,QAAQ,GAAG;AACxG,aAAO,SAAS;AAChB,cAAQ,KAAK,0BAA0B,QAAQ;AAAA,IACjD;AACA,QACE,SAAS,eAAe,UACxB,SAAS,WAAW,SAAS,KAC7B,CAAC,KAAK,oBAAoB,IAAI,0BAA0B,UAAU,GAClE;AACA,aAAO,SAAS;AAChB,cAAQ,KAAK,0BAA0B,UAAU;AAAA,IACnD;AAEA,WAAO,KAAK,gBAAgB,UAAU,OAAO;AAAA,EAC/C;AAAA,EAEA,MAAc,gBAAgB,MAA4B,SAAiD;AACzG,QAAI,KAAK,SAAS,WAAc,KAAK,eAAe,UAAa,KAAK,gBAAgB,SAAY;AAChG,YAAM,IAAI,MAAM,8GAAyG;AAAA,IAC3H;AACA,UAAM,QAAQ,KAAK,SAAS,KAAK;AAEjC,QAAI,KAAK,aAAa,SAAS;AAC7B,aAAO,KAAK,qBAAqB,MAAM,OAAO,OAAO;AAAA,IACvD;AAEA,UAAM,cAAyC,EAAE,OAAO,QAAQ,KAAK,QAAQ,eAAe,MAAM;AAClG,QAAI,KAAK,MAAM,OAAW,aAAY,IAAI,KAAK;AAC/C,QAAI,KAAK,SAAS,OAAW,aAAY,OAAO,KAAK;AACrD,QAAI,KAAK,eAAe,OAAW,aAAY,aAAa,KAAK;AACjE,QAAI,KAAK,gBAAgB,OAAW,aAAY,eAAe,KAAK;AACpE,QAAI,KAAK,YAAY,OAAW,aAAY,UAAU,KAAK;AAC3D,QAAI,KAAK,eAAe,OAAW,aAAY,aAAa,KAAK;AACjE,QAAI,KAAK,SAAS,OAAW,aAAY,OAAO,KAAK;AACrD,QAAI,KAAK,UAAU,OAAW,aAAY,QAAQ,KAAK;AACvD,QAAI,KAAK,eAAe,UAAa,KAAK,WAAW,SAAS,GAAG;AAC/D,kBAAY,mBAAmB,KAAK,WAAW,IAAI,CAAC,OAAO,EAAE,MAAM,aAAa,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;AAAA,IAC9G;AACA,QAAI,KAAK,aAAa,QAAW;AAC/B,YAAM,gBAA2C,CAAC;AAClD,UAAI,KAAK,SAAS,UAAU,OAAW,eAAc,QAAQ,KAAK,SAAS;AAC3E,UAAI,KAAK,SAAS,SAAS,OAAW,eAAc,OAAO,KAAK,SAAS;AACzE,UAAI,KAAK,SAAS,mBAAmB,OAAW,eAAc,kBAAkB,KAAK,SAAS;AAC9F,kBAAY,WAAW;AAAA,IACzB;AAEA,WAAO,KAAK,aAAa,GAAG,KAAK,OAAO,WAAW,aAAa,OAAO,OAAO;AAAA,EAChF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,qBACZ,MACA,OACA,SAC8B;AAC9B,UAAM,cAAyC,EAAE,OAAO,QAAQ,KAAK,OAAO;AAC5E,QAAI,KAAK,MAAM,OAAW,aAAY,IAAI,KAAK;AAC/C,QAAI,KAAK,SAAS,OAAW,aAAY,OAAO,KAAK;AACrD,QAAI,KAAK,SAAS,OAAW,aAAY,OAAO,KAAK;AACrD,QAAI,KAAK,UAAU,OAAW,aAAY,QAAQ,KAAK;AACvD,QAAI,KAAK,aAAa,OAAW,aAAY,iBAAiB,KAAK;AACnE,QAAI,KAAK,eAAe,UAAa,KAAK,WAAW,SAAS,GAAG;AAC/D,kBAAY,mBAAmB,KAAK,WAAW,IAAI,CAAC,OAAO,EAAE,MAAM,aAAa,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;AAAA,IAC9G;AACA,eAAW,SAAS,CAAC,cAAc,eAAe,WAAW,YAAY,GAAY;AACnF,UAAI,KAAK,KAAK,MAAM,OAAW,SAAQ,KAAK,KAAK;AAAA,IACnD;AACA,QAAI,KAAK,aAAa,OAAW,SAAQ,KAAK,UAAU;AAExD,WAAO,KAAK,aAAa,GAAG,KAAK,OAAO,uBAAuB,aAAa,OAAO,OAAO;AAAA,EAC5F;AAAA,EAEQ,cAAsC;AAC5C,WAAO,KAAK,SAAS,EAAE,eAAe,UAAU,KAAK,MAAM,GAAG,IAAI,CAAC;AAAA,EACrE;AAAA,EAEA,MAAc,aACZ,KACA,aACA,OACA,SAC8B;AAC9B,UAAM,WAAW,MAAM,KAAK,eAAe,KAAK;AAAA,MAC9C,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oBAAoB,GAAG,KAAK,YAAY,EAAE;AAAA,MACrE,MAAM,KAAK,UAAU,WAAW;AAAA,IAClC,CAAC;AACD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,WAAW;AAC/D,YAAM,IAAI;AAAA,QACR,gBAAgB,KAAK,QAAQ,aAAa,SAAS,MAAM,IAAI,SAAS,UAAU,KAAK,SAAS;AAAA,MAChG;AAAA,IACF;AACA,UAAM,UAAU,MAAM,SAAS,KAAK;AACpC,UAAM,OAAO,cAAc,KAAK,UAAU,OAAO;AACjD,QAAI,CAAC,aAAa,IAAI,GAAG;AACvB,YAAM,IAAI,MAAM,gBAAgB,KAAK,QAAQ,+BAA+B,OAAO,EAAE;AAAA,IACvF;AACA,QAAI,aAAa,KAAK,KAAK,GAAG;AAC5B,YAAM,SAAS,SAAS,KAAK,MAAM,OAAO,KAAK;AAC/C,YAAM,IAAI,MAAM,gBAAgB,KAAK,QAAQ,uBAAuB,MAAM,EAAE;AAAA,IAC9E;AACA,QAAI,CAAC,MAAM,QAAQ,KAAK,IAAI,GAAG;AAC7B,YAAM,IAAI,MAAM,gBAAgB,KAAK,QAAQ,oCAAoC,OAAO,EAAE;AAAA,IAC5F;AAEA,UAAM,SAA2B,KAAK,KAAK,IAAI,CAAC,OAAO,MAAM;AAC3D,UAAI,CAAC,aAAa,KAAK,GAAG;AACxB,cAAM,IAAI,MAAM,gBAAgB,KAAK,QAAQ,kBAAkB,CAAC,uBAAuB,OAAO,EAAE;AAAA,MAClG;AACA,YAAM,MAAM,SAAS,MAAM,QAAQ;AACnC,UAAI,CAAC,KAAK;AACR,cAAM,IAAI,MAAM,gBAAgB,KAAK,QAAQ,kBAAkB,CAAC,2BAA2B,OAAO,EAAE;AAAA,MACtG;AACA,aAAO,EAAE,OAAO,aAAa,GAAG,GAAG,WAAW,SAAS,MAAM,UAAU,KAAK,YAAY;AAAA,IAC1F,CAAC;AAED,UAAM,WAAW,aAAa,KAAK,KAAK,IAAI,KAAK,QAAQ;AACzD,UAAM,QAAQ;AAAA,MACZ,MAAM,SAAS,UAAU,IAAI,KAAK;AAAA,MAClC,aAAa,SAAS,UAAU,YAAY,KAAK;AAAA,IACnD;AAEA,WAAO,EAAE,QAAQ,OAAO,OAAO,QAAQ;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,eAAe,KAAa,MAAuD;AAC/F,UAAM,cAAc,KAAK,aAAa,UAAU,IAAI;AACpD,QAAI;AACJ,QAAI;AACJ,aAAS,UAAU,GAAG,WAAW,aAAa,WAAW;AACvD,UAAI;AACF,mBAAW,MAAM,KAAK,QAAQ,KAAK,IAAI;AAAA,MACzC,SAAS,GAAG;AACV,mBAAW;AACX,2BAAmB,aAAa,QAAQ,IAAI,IAAI,MAAM,OAAO,CAAC,CAAC;AAAA,MACjE;AACA,UAAI,aAAa,UAAa,SAAS,SAAS,IAAK,QAAO;AAC5D,YAAM,UAAU,uBAAuB,UAAU,CAAC;AAClD,UAAI,YAAY,UAAa,WAAW,YAAa;AACrD,YAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC;AAAA,IACjD;AACA,QAAI,SAAU,QAAO;AACrB,UAAM,oBAAoB,IAAI,MAAM,gBAAgB,KAAK,QAAQ,yBAAyB,WAAW,WAAW;AAAA,EAClH;AAAA,EAOA,OAAe,qBAAkC;AAC/C,QAAI,aAAY,oBAAoB,MAAM;AACxC,mBAAY,kBAAkB,IAAI,YAAY,EAAE,gBAAgB,GAAG,aAAa,EAAE,CAAC;AAAA,IACrF;AACA,WAAO,aAAY;AAAA,EACrB;AAAA,EAEA,MAAc,QAAQ,KAAa,MAAuD;AACxF,QAAI,KAAK,aAAa,WAAW,KAAK,cAAc,OAAO;AACzD,YAAM,QAA2B;AAAA,QAC/B,QAAQ,KAAK;AAAA,QACb,SAAS,KAAK;AAAA,QACd,MAAM,KAAK;AAAA,QACX,YAAY,aAAY,mBAAmB;AAAA,MAC7C;AACA,aAAO,YAAY,KAAK,KAAK;AAAA,IAC/B;AACA,WAAO,KAAK,UAAU,KAAK,IAAI;AAAA,EACjC;AACF;AAAA;AAAA;AAAA;AA7Qa,aAwPI,kBAAsC;AAxPhD,IAAM,cAAN;AA+QA,IAAM,yBAA6G;AAAA,EACxH;AAAA,IACE,UAAU;AAAA,IACV,QAAQ,OAAO,OAAO,uBAAuB;AAAA,IAC7C,cAAc,0BAA0B;AAAA,EAC1C;AAAA,EACA;AAAA,IACE,UAAU;AAAA,IACV,QAAQ,OAAO,OAAO,kBAAkB;AAAA,IACxC,cAAc,qBAAqB;AAAA,EACrC;AACF;;;ACvaO,SAAS,iBACd,UACA,cACkB;AAClB,MAAI,iBAAiB,UAAU;AAC7B,UAAMC,eAAc,cAAc,QAAQ;AAC1C,WAAO;AAAA,MACL,aAAa;AAAA,MACb,QAAQ;AAAA,MACR,gBAAgB;AAAA,MAChB,qBAAqB,wBAAwB,QAAQ;AAAA,MACrD,sBAAsBA,aAAY;AAAA,MAClC,oBAAoBA,aAAY;AAAA,IAClC;AAAA,EACF;AAEA,MAAI;AACF,SAAK,MAAM,QAAQ;AACnB,WAAO,EAAE,aAAa,OAAO,QAAQ,OAAO;AAAA,EAC9C,QAAQ;AAAA,EAER;AAEA,MAAI,iBAAiB,UAAU,iBAAiB,MAAM;AACpD,UAAM,UAAU,SAAS,KAAK;AAE9B,UAAM,eACJ,QAAQ,SAAS,GAAG,KACpB,QAAQ,SAAS,GAAG,KACpB,QAAQ,SAAS,KAAK,KACtB,QAAQ,KAAK,OAAO,KACpB,QAAQ,KAAK,OAAO;AAEtB,QAAI,cAAc;AAChB,YAAMA,eAAc,cAAc,QAAQ;AAC1C,aAAO;AAAA,QACL,aAAa;AAAA,QACb,QAAQ;AAAA,QACR,gBAAgB;AAAA,QAChB,qBAAqB,wBAAwB,QAAQ;AAAA,QACrD,sBAAsBA,aAAY;AAAA,QAClC,oBAAoBA,aAAY;AAAA,MAClC;AAAA,IACF;AAEA,QAAI;AACF,YAAM,SAAS,cAAc,OAAO;AACpC,WAAK,MAAM,MAAM;AACjB,aAAO,EAAE,aAAa,OAAO,QAAQ,OAAO;AAAA,IAC9C,QAAQ;AACN,aAAO,EAAE,aAAa,OAAO,QAAQ,OAAO;AAAA,IAC9C;AAAA,EACF;AAEA,QAAM,cAAc,cAAc,QAAQ;AAC1C,MACE,YAAY,uBAAuB,KACnC,YAAY,qBAAqB,GACjC;AACA,WAAO;AAAA,MACL,aAAa;AAAA,MACb,QAAQ;AAAA,MACR,gBAAgB;AAAA,MAChB,qBAAqB,wBAAwB,QAAQ;AAAA,MACrD,sBAAsB,YAAY;AAAA,MAClC,oBAAoB,YAAY;AAAA,IAClC;AAAA,EACF;AAEA,SAAO,EAAE,aAAa,OAAO,QAAQ,OAAO;AAC9C;AAMA,SAAS,cAAc,MAOrB;AACA,MAAI,WAAW;AACf,MAAI,UAAU;AACd,MAAI,eAAe;AACnB,MAAI,gBAAgB;AACpB,MAAI,aAAa;AACjB,MAAI,cAAc;AAElB,aAAW,QAAQ,MAAM;AACvB,QAAI,SAAS;AACX,gBAAU;AACV;AAAA,IACF;AACA,QAAI,SAAS,QAAQ,UAAU;AAC7B,gBAAU;AACV;AAAA,IACF;AACA,QAAI,SAAS,KAAK;AAChB,iBAAW,CAAC;AACZ;AAAA,IACF;AACA,QAAI,SAAU;AAEd,YAAQ,MAAM;AAAA,MACZ,KAAK;AACH;AACA;AAAA,MACF,KAAK;AACH;AACA;AAAA,MACF,KAAK;AACH;AACA;AAAA,MACF,KAAK;AACH;AACA;AAAA,IACJ;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,sBAAsB,KAAK,IAAI,GAAG,eAAe,aAAa;AAAA,IAC9D,oBAAoB,KAAK,IAAI,GAAG,aAAa,WAAW;AAAA,EAC1D;AACF;AAEO,SAAS,wBAAwB,MAA8B;AACpE,QAAM,aAAa,cAAc,IAAI;AACrC,MAAI;AACF,WAAO,KAAK,MAAM,UAAU;AAAA,EAC9B,QAAQ;AAAA,EAER;AAEA,QAAM,UAAU,KAAK,KAAK;AAE1B,MAAI,QAAQ,WAAW,GAAG,GAAG;AAC3B,UAAM,oBAAoB,6BAA6B,OAAO;AAC9D,QAAI,oBAAoB,GAAG;AACzB,YAAM,SAAS,QAAQ,UAAU,GAAG,iBAAiB,IAAI;AACzD,UAAI;AACF,eAAO,KAAK,MAAM,MAAM;AAAA,MAC1B,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,MAAI,QAAQ,WAAW,GAAG,GAAG;AAC3B,UAAM,SAAS,cAAc,OAAO;AACpC,QAAI;AACF,aAAO,KAAK,MAAM,MAAM;AAAA,IAC1B,QAAQ;AACN,YAAM,oBAAoB,+BAA+B,OAAO;AAChE,UAAI,oBAAoB,GAAG;AACzB,cAAM,SAAS,QAAQ,UAAU,GAAG,iBAAiB,IAAI;AACzD,YAAI;AACF,iBAAO,KAAK,MAAM,MAAM;AAAA,QAC1B,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,6BAA6B,MAAsB;AAC1D,MAAI,QAAQ;AACZ,MAAI,WAAW;AACf,MAAI,UAAU;AACd,MAAI,yBAAyB;AAE7B,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,OAAO,KAAK,CAAC;AAEnB,QAAI,SAAS;AACX,gBAAU;AACV;AAAA,IACF;AACA,QAAI,SAAS,QAAQ,UAAU;AAC7B,gBAAU;AACV;AAAA,IACF;AACA,QAAI,SAAS,KAAK;AAChB,iBAAW,CAAC;AACZ;AAAA,IACF;AACA,QAAI,SAAU;AAEd,QAAI,SAAS,OAAO,SAAS,KAAK;AAChC;AAAA,IACF,WAAW,SAAS,OAAO,SAAS,KAAK;AACvC;AACA,UAAI,UAAU,GAAG;AACf,iCAAyB,IAAI;AAAA,MAC/B;AAAA,IACF,WAAW,SAAS,OAAO,UAAU,GAAG;AACtC,+BAAyB;AAAA,IAC3B;AAAA,EACF;AAEA,SAAO,yBAAyB,IAAI,yBAAyB;AAC/D;AAEA,SAAS,+BAA+B,MAAsB;AAC5D,MAAI,QAAQ;AACZ,MAAI,WAAW;AACf,MAAI,UAAU;AACd,MAAI,iBAAiB;AAErB,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,OAAO,KAAK,CAAC;AAEnB,QAAI,SAAS;AACX,gBAAU;AACV;AAAA,IACF;AACA,QAAI,SAAS,QAAQ,UAAU;AAC7B,gBAAU;AACV;AAAA,IACF;AACA,QAAI,SAAS,KAAK;AAChB,iBAAW,CAAC;AACZ;AAAA,IACF;AACA,QAAI,SAAU;AAEd,QAAI,SAAS,OAAO,SAAS,KAAK;AAChC;AAAA,IACF,WAAW,SAAS,OAAO,SAAS,KAAK;AACvC;AAAA,IACF,WAAW,SAAS,OAAO,UAAU,GAAG;AACtC,uBAAiB;AAAA,IACnB;AAAA,EACF;AAEA,SAAO,iBAAiB,IAAI,iBAAiB;AAC/C;AAEO,SAAS,kBAAkB,SAA0B;AAC1D,QAAM,UAAU,QAAQ,KAAK;AAC7B,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,WAAW,cAAc,OAAO;AACtC,MACE,SAAS,uBAAuB,KAChC,SAAS,qBAAqB,GAC9B;AACA,WAAO;AAAA,EACT;AAEA,QAAM,gBAAgB;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,aAAW,WAAW,eAAe;AACnC,QAAI,QAAQ,KAAK,OAAO,EAAG,QAAO;AAAA,EACpC;AAEA,SAAO;AACT;;;AC7QA,IAAM,qBAAqB;AAC3B,IAAM,4BAA4B;AAMlC,IAAM,qCAAqC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBpC,SAAS,eACd,UACA,cACQ;AACR,QAAM,cAAc,SAAS,QAAQ;AACrC,QAAM,cAAc,aAAa,UAAU;AAE3C,MAAI,cAAc,YACf,QAAQ,iBAAiB,EAAE,EAC3B,QAAQ,YAAY,EAAE,EACtB,KAAK;AAER,MAAI,YAAY,WAAW,GAAG,GAAG;AAC/B,QAAI;AACF,YAAM,aAAa,KAAK,MAAM,cAAc,WAAW,CAAC;AACxD,YAAM,OAAO,OAAO,KAAK,UAAU;AACnC,UAAI,KAAK,WAAW,KAAK,MAAM,QAAQ,WAAW,KAAK,CAAC,CAAC,CAAC,GAAG;AAC3D,sBAAc,WAAW,KAAK,CAAC,CAAC,EAC7B,IAAI,CAAC,SAAkB,KAAK,UAAU,IAAI,CAAC,EAC3C,KAAK,KAAK;AAAA,MACf;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,YAAY,WAAW,GAAG,KAAK,YAAY,WAAW,GAAG,GAAG;AAC9D,WAAO,cAAc;AAAA,EACvB;AAEA,QAAM,oBAAoB,cAAc,KAAK,WAAW;AACxD,QAAM,sBAAsB,YAAY,KAAK,WAAW;AAExD,MAAI,qBAAqB,qBAAqB;AAC5C,WAAO,cAAc,QAAQ;AAAA,EAC/B;AAEA,SAAO,cAAc;AACvB;AAEO,SAAS,uBAA0B,aAA+B;AACvE,UAAQ,KAAK,uDAAuD;AAEpE,MAAI;AACF,UAAM,kBAAkB,oBAAoB,WAAW,KAAK;AAC5D,UAAM,SAAS,cAAc,eAAe;AAC5C,UAAM,SAAS,KAAK,MAAM,MAAM;AAChC,YAAQ,IAAI,uDAAuD;AACnE,WAAO;AAAA,EACT,SAAS,OAAO;AACd,YAAQ,MAAM,8CAA8C,KAAK;AAAA,EACnE;AAEA,SAAO;AACT;AAgBA,eAAsB,qBACpB,SACgC;AAChC,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,mBAAmB;AAAA,IACnB;AAAA,IACA,2BAA2B;AAAA,EAC7B,IAAI;AAEJ,MAAI,cAAc;AAClB,MAAI,oBAAoB;AACxB,QAAM,WAAqB,CAAC;AAC5B,MAAI,cAAc;AAElB,UAAQ,IAAI,4DAA4D;AACxE,UAAQ;AAAA,IACN,8BAA8B,SAAS,wBAAwB,gBAAgB;AAAA,EACjF;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,OAAO,oBAAoB;AAAA,MAChD;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,kBAAc,oBAAoB,SAAS,GAAG,KAAK,SAAS;AAE5D,YAAQ;AAAA,MACN,oCAAoC,YAAY,MAAM,0BAA0B,SAAS,YAAY;AAAA,IACvG;AAEA,QAAI,aAAa,iBAAiB,aAAa,SAAS,YAAY;AAEpE,WAAO,WAAW,eAAe,oBAAoB,kBAAkB;AACrE;AACA,YAAM,aAAa,uBAAuB,WAAW,MAAM,0BAA0B,iBAAiB,IAAI,gBAAgB;AAC1H,cAAQ,IAAI,kBAAkB,UAAU,EAAE;AAC1C,eAAS,KAAK,UAAU;AAExB,YAAM,aAAa;AAAA,QACjB;AAAA,QACA;AAAA,MACF;AAEA,YAAM,eAAe,MAAM,OAAO,oBAAoB;AAAA,QACpD,cAAc;AAAA,QACd,YAAY;AAAA,QACZ;AAAA,MACF,CAAC;AAED,cAAQ;AAAA,QACN,yCAAyC,aAAa,IAAI,MAAM,0BAA0B,aAAa,YAAY;AAAA,MACrH;AAEA,YAAM,sBACJ,oBAAoB,aAAa,GAAG,KAAK,aAAa;AACxD,oBAAc,eAAe,aAAa,mBAAmB;AAE7D,mBAAa,iBAAiB,aAAa,aAAa,YAAY;AAAA,IACtE;AAEA,QACE,qBAAqB,oBACrB,WAAW,aACX;AACA,cAAQ;AAAA,QACN,6CAA6C,gBAAgB;AAAA,MAC/D;AACA,eAAS;AAAA,QACP;AAAA,MACF;AACA,oBAAc;AAAA,IAChB;AAEA,UAAM,kBACJ,oBAAoB,WAAW,KAAK;AACtC,QAAI;AAEJ,QAAI;AACF,UAAI,YAAY,eAAe,GAAG;AAChC,eAAO,KAAK,MAAM,eAAe;AAAA,MACnC,OAAO;AACL,cAAM,SAAS,cAAc,eAAe;AAC5C,eAAO,KAAK,MAAM,MAAM;AACxB,YAAI,CAAC,aAAa;AAChB,mBAAS,KAAK,iDAAiD;AAAA,QACjE;AAAA,MACF;AAAA,IACF,SAAS,YAAY;AACnB,YAAM,WAAW,uBAA0B,eAAe;AAC1D,UAAI,UAAU;AACZ,eAAO;AACP,sBAAc;AACd,iBAAS,KAAK,yCAAyC;AAAA,MACzD,OAAO;AACL,cAAM,IAAI;AAAA,UACR,kCAAkC,iBAAiB,mBAAmB,UAAU;AAAA,QAClF;AAAA,MACF;AAAA,IACF;AAEA,QAAI,QAAQ;AACV,UAAI;AACF,eAAO,OAAO,MAAM,IAAI;AAAA,MAC1B,SAAS,iBAAiB;AACxB,gBAAQ;AAAA,UACN;AAAA,UACA;AAAA,QACF;AACA,iBAAS,KAAK,4BAA4B,eAAe,EAAE;AAAA,MAC7D;AAAA,IACF;AAEA,YAAQ;AAAA,MACN,2CAA2C,iBAAiB,eAAe,SAAS,MAAM;AAAA,IAC5F;AAEA,WAAO;AAAA,MACL;AAAA,MACA,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,YAAQ,MAAM,yCAAyC,KAAK;AAC5D,UAAM;AAAA,EACR;AACF;AAcO,SAAS,+BACd,SACA,iBACA,SACA,cAAsB,2BACd;AACR,SAAO,oCAAoC,OAAO,IAAI,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,EAKjE,OAAO;AAAA;AAAA;AAAA;AAAA,EAIP,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUjB;","names":["DEFAULT_MODELS","DEFAULT_BASE_URLS","API_KEY_ENV_VARS","bracketInfo"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/llm",
3
- "version": "2.49.0",
3
+ "version": "2.51.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",
@@ -47,10 +47,10 @@
47
47
  "zod": "^3.22.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "@almadar/core": "^10.87.0"
50
+ "@almadar/core": "^10.89.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@almadar/core": "^10.87.0",
53
+ "@almadar/core": "^10.89.0",
54
54
  "@almadar/eslint-plugin": "^2.18.0",
55
55
  "@types/node": "^22.0.0",
56
56
  "@typescript-eslint/parser": "8.65.0",
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Image Client
3
3
  *
4
- * Single-purpose client for OpenRouter's image-generation API
5
- * (`POST {baseUrl}/images`). Raw `fetch`, no SDK — mirrors
4
+ * Single-purpose client for image-generation APIs. `openrouter` posts to
5
+ * `POST {baseUrl}/images`; `local` posts an OpenAI-shaped body to
6
+ * `POST {baseUrl}/images/generations` (the MFLUX shim in
7
+ * tools/asset-workflow/python/image_server). Raw `fetch`, no SDK — mirrors
6
8
  * `embedding-client.ts`'s shape (provider tables, retry policy, the
7
9
  * three response-error surfaces).
8
10
  *
@@ -10,10 +12,11 @@
10
12
  */
11
13
 
12
14
  import type { JsonValue } from '@almadar/core';
15
+ import { Agent as UndiciAgent, fetch as undiciFetch, type RequestInit as UndiciRequestInit, type Response as UndiciResponse } from 'undici';
13
16
 
14
- export type ImageProvider = 'openrouter';
17
+ export type ImageProvider = 'openrouter' | 'local';
15
18
 
16
- const IMAGE_PROVIDERS: readonly ImageProvider[] = ['openrouter'];
19
+ const IMAGE_PROVIDERS: readonly ImageProvider[] = ['openrouter', 'local'];
17
20
 
18
21
  /** Canonical image-provider resolution, mirrors `resolveDefaultEmbeddingProvider`. */
19
22
  export function resolveDefaultImageProvider(): ImageProvider {
@@ -34,6 +37,20 @@ export const OPENROUTER_IMAGE_DEFAULTS = {
34
37
  preview: OPENROUTER_IMAGE_MODELS.GEMINI_3_1_FLASH_LITE_IMAGE,
35
38
  } as const;
36
39
 
40
+ export const LOCAL_IMAGE_MODELS = {
41
+ Z_IMAGE_TURBO: 'z-image-turbo',
42
+ FLUX_SCHNELL: 'flux-schnell',
43
+ FLUX_DEV: 'flux-dev',
44
+ FLUX_KONTEXT: 'flux-kontext',
45
+ FLUX2_KLEIN: 'flux2-klein',
46
+ } as const;
47
+
48
+ export const LOCAL_IMAGE_DEFAULTS = {
49
+ /** Z-Image-Turbo is the default: public on Hugging Face, no token required (FLUX is gated). */
50
+ sheet: process.env.LOCAL_IMAGE_SHEET_MODEL ?? LOCAL_IMAGE_MODELS.Z_IMAGE_TURBO,
51
+ preview: process.env.LOCAL_IMAGE_PREVIEW_MODEL ?? LOCAL_IMAGE_MODELS.Z_IMAGE_TURBO,
52
+ };
53
+
37
54
  export interface ImageClientOptions {
38
55
  provider?: ImageProvider;
39
56
  model?: string;
@@ -57,6 +74,10 @@ export interface ImageGenerateOptions {
57
74
  quality?: 'auto' | 'low' | 'medium' | 'high';
58
75
  background?: 'auto' | 'transparent' | 'opaque';
59
76
  seed?: number;
77
+ /** Diffusion step count (local shim; 1–100). */
78
+ steps?: number;
79
+ /** img2img denoise strength 0–1 (local shim `image_strength`), used with `references`. */
80
+ strength?: number;
60
81
  references?: ReadonlyArray<ImageReference>;
61
82
  provider?: { order?: string[]; only?: string[]; allowFallbacks?: boolean };
62
83
  }
@@ -85,14 +106,17 @@ const IMAGE_RETRY_BACKOFF_MS = [500, 2000];
85
106
 
86
107
  const DEFAULT_MODELS: Record<ImageProvider, string> = {
87
108
  openrouter: OPENROUTER_IMAGE_DEFAULTS.sheet,
109
+ local: LOCAL_IMAGE_DEFAULTS.sheet,
88
110
  };
89
111
 
90
112
  const DEFAULT_BASE_URLS: Record<ImageProvider, string> = {
91
113
  openrouter: 'https://openrouter.ai/api/v1',
114
+ local: 'http://127.0.0.1:8180/v1',
92
115
  };
93
116
 
94
117
  const API_KEY_ENV_VARS: Record<ImageProvider, string> = {
95
118
  openrouter: 'OPEN_ROUTER_API_KEY',
119
+ local: 'LOCAL_IMAGE_API_KEY',
96
120
  };
97
121
 
98
122
  /**
@@ -103,6 +127,8 @@ const API_KEY_ENV_VARS: Record<ImageProvider, string> = {
103
127
  const IMAGE_CHECKED_PARAM_NAMES = {
104
128
  background: 'background',
105
129
  seed: 'seed',
130
+ steps: 'steps',
131
+ strength: 'image_strength',
106
132
  references: 'input_references',
107
133
  } as const;
108
134
 
@@ -167,13 +193,14 @@ export class ImageClient {
167
193
  private readonly capabilitiesCache = new Map<string, ImageModelCapabilities>();
168
194
 
169
195
  constructor(opts: ImageClientOptions = {}) {
170
- this.provider = opts.provider ?? 'openrouter';
196
+ this.provider = opts.provider ?? resolveDefaultImageProvider();
171
197
  this.model = opts.model ?? DEFAULT_MODELS[this.provider];
172
198
  this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URLS[this.provider]).replace(/\/$/, '');
173
199
  this.fetchImpl = opts.fetchImpl ?? fetch;
174
200
 
175
- const apiKey = opts.apiKey ?? process.env[API_KEY_ENV_VARS[this.provider]];
176
- if (!apiKey) {
201
+ // The local shim is unauthenticated; a key is required only for hosted providers.
202
+ const apiKey = opts.apiKey ?? process.env[API_KEY_ENV_VARS[this.provider]] ?? '';
203
+ if (!apiKey && this.provider !== 'local') {
177
204
  throw new Error(
178
205
  `ImageClient: API key for provider '${this.provider}' not found. ` +
179
206
  `Set ${API_KEY_ENV_VARS[this.provider]} in your environment or pass options.apiKey.`,
@@ -197,9 +224,20 @@ export class ImageClient {
197
224
  const cached = this.capabilitiesCache.get(model);
198
225
  if (cached) return cached;
199
226
 
227
+ // The local shim serves no `.../endpoints` metadata; declare what it honors.
228
+ if (this.provider === 'local') {
229
+ const local: ImageModelCapabilities = {
230
+ model,
231
+ supportedParameters: new Set(['seed', IMAGE_CHECKED_PARAM_NAMES.steps, IMAGE_CHECKED_PARAM_NAMES.strength, IMAGE_CHECKED_PARAM_NAMES.references]),
232
+ raw: {},
233
+ };
234
+ this.capabilitiesCache.set(model, local);
235
+ return local;
236
+ }
237
+
200
238
  const response = await this.fetchWithRetry(`${this.baseUrl}/images/models/${model}/endpoints`, {
201
239
  method: 'GET',
202
- headers: { Authorization: `Bearer ${this.apiKey}` },
240
+ headers: this.authHeaders(),
203
241
  });
204
242
  if (!response.ok) {
205
243
  const errorText = await response.text().catch(() => '<no body>');
@@ -233,6 +271,14 @@ export class ImageClient {
233
271
  delete filtered.seed;
234
272
  dropped.push(IMAGE_CHECKED_PARAM_NAMES.seed);
235
273
  }
274
+ if (filtered.steps !== undefined && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.steps)) {
275
+ delete filtered.steps;
276
+ dropped.push(IMAGE_CHECKED_PARAM_NAMES.steps);
277
+ }
278
+ if (filtered.strength !== undefined && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.strength)) {
279
+ delete filtered.strength;
280
+ dropped.push(IMAGE_CHECKED_PARAM_NAMES.strength);
281
+ }
236
282
  if (
237
283
  filtered.references !== undefined &&
238
284
  filtered.references.length > 0 &&
@@ -250,6 +296,11 @@ export class ImageClient {
250
296
  throw new Error('ImageClient.generate: `size` cannot be combined with `resolution`/`aspectRatio` — pass one or the other');
251
297
  }
252
298
  const model = opts.model ?? this.model;
299
+
300
+ if (this.provider === 'local') {
301
+ return this.performLocalGenerate(opts, model, dropped);
302
+ }
303
+
253
304
  const requestBody: Record<string, JsonValue> = { model, prompt: opts.prompt, output_format: 'png' };
254
305
  if (opts.n !== undefined) requestBody.n = opts.n;
255
306
  if (opts.size !== undefined) requestBody.size = opts.size;
@@ -258,6 +309,7 @@ export class ImageClient {
258
309
  if (opts.quality !== undefined) requestBody.quality = opts.quality;
259
310
  if (opts.background !== undefined) requestBody.background = opts.background;
260
311
  if (opts.seed !== undefined) requestBody.seed = opts.seed;
312
+ if (opts.steps !== undefined) requestBody.steps = opts.steps;
261
313
  if (opts.references !== undefined && opts.references.length > 0) {
262
314
  requestBody.input_references = opts.references.map((r) => ({ type: 'image_url', image_url: { url: r.url } }));
263
315
  }
@@ -269,9 +321,50 @@ export class ImageClient {
269
321
  requestBody.provider = providerField;
270
322
  }
271
323
 
272
- const response = await this.fetchWithRetry(`${this.baseUrl}/images`, {
324
+ return this.sendGenerate(`${this.baseUrl}/images`, requestBody, model, dropped);
325
+ }
326
+
327
+ /**
328
+ * OpenAI-compatible local server (the MFLUX shim in tools/asset-workflow).
329
+ * OpenRouter-only fields are reported in `dropped`, never sent. References
330
+ * ARE sent (as `input_references`) — the shim forwards them to mflux's
331
+ * `image_path`/`image_strength` img2img conditioning.
332
+ */
333
+ private async performLocalGenerate(
334
+ opts: ImageGenerateOptions,
335
+ model: string,
336
+ dropped: string[],
337
+ ): Promise<ImageGenerateResult> {
338
+ const requestBody: Record<string, JsonValue> = { model, prompt: opts.prompt };
339
+ if (opts.n !== undefined) requestBody.n = opts.n;
340
+ if (opts.size !== undefined) requestBody.size = opts.size;
341
+ if (opts.seed !== undefined) requestBody.seed = opts.seed;
342
+ if (opts.steps !== undefined) requestBody.steps = opts.steps;
343
+ if (opts.strength !== undefined) requestBody.image_strength = opts.strength;
344
+ if (opts.references !== undefined && opts.references.length > 0) {
345
+ requestBody.input_references = opts.references.map((r) => ({ type: 'image_url', image_url: { url: r.url } }));
346
+ }
347
+ for (const field of ['resolution', 'aspectRatio', 'quality', 'background'] as const) {
348
+ if (opts[field] !== undefined) dropped.push(field);
349
+ }
350
+ if (opts.provider !== undefined) dropped.push('provider');
351
+
352
+ return this.sendGenerate(`${this.baseUrl}/images/generations`, requestBody, model, dropped);
353
+ }
354
+
355
+ private authHeaders(): Record<string, string> {
356
+ return this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {};
357
+ }
358
+
359
+ private async sendGenerate(
360
+ url: string,
361
+ requestBody: Record<string, JsonValue>,
362
+ model: string,
363
+ dropped: string[],
364
+ ): Promise<ImageGenerateResult> {
365
+ const response = await this.fetchWithRetry(url, {
273
366
  method: 'POST',
274
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.apiKey}` },
367
+ headers: { 'Content-Type': 'application/json', ...this.authHeaders() },
275
368
  body: JSON.stringify(requestBody),
276
369
  });
277
370
  if (!response.ok) {
@@ -313,24 +406,52 @@ export class ImageClient {
313
406
  return { images, usage, model, dropped };
314
407
  }
315
408
 
316
- /** Retries on network failure or a 5xx status; any other response is returned as-is. */
317
- private async fetchWithRetry(url: string, init: RequestInit): Promise<Response> {
409
+ /** Retries on network failure or a 5xx status; any other response is returned as-is.
410
+ * The local provider never retries: a failed local generation (e.g. MLX OOM) cannot
411
+ * succeed on an immediate retry, and hammering a memory-pressured machine is harmful. */
412
+ private async fetchWithRetry(url: string, init: RequestInit): Promise<Response | UndiciResponse> {
413
+ const maxAttempts = this.provider === 'local' ? 1 : IMAGE_MAX_ATTEMPTS;
318
414
  let lastNetworkError: Error | undefined;
319
- let response: Response | undefined;
320
- for (let attempt = 1; attempt <= IMAGE_MAX_ATTEMPTS; attempt++) {
415
+ let response: Response | UndiciResponse | undefined;
416
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
321
417
  try {
322
- response = await this.fetchImpl(url, init);
418
+ response = await this.doFetch(url, init);
323
419
  } catch (e) {
324
420
  response = undefined;
325
421
  lastNetworkError = e instanceof Error ? e : new Error(String(e));
326
422
  }
327
423
  if (response !== undefined && response.status < 500) return response;
328
424
  const backoff = IMAGE_RETRY_BACKOFF_MS[attempt - 1];
329
- if (backoff === undefined) break;
425
+ if (backoff === undefined || attempt >= maxAttempts) break;
330
426
  await new Promise((r) => setTimeout(r, backoff));
331
427
  }
332
428
  if (response) return response;
333
- throw lastNetworkError ?? new Error(`ImageClient: ${this.provider} request failed after ${IMAGE_MAX_ATTEMPTS} attempts`);
429
+ throw lastNetworkError ?? new Error(`ImageClient: ${this.provider} request failed after ${maxAttempts} attempts`);
430
+ }
431
+
432
+ /** Local generations can exceed Node's built-in 300s headersTimeout (a single
433
+ * MFLUX image routinely takes minutes), so local calls ride npm-undici's own
434
+ * fetch with the transport timers disabled — mirrors `LLMClient.localDispatcher`. */
435
+ private static localDispatcher: UndiciAgent | null = null;
436
+
437
+ private static getLocalDispatcher(): UndiciAgent {
438
+ if (ImageClient.localDispatcher === null) {
439
+ ImageClient.localDispatcher = new UndiciAgent({ headersTimeout: 0, bodyTimeout: 0 });
440
+ }
441
+ return ImageClient.localDispatcher;
442
+ }
443
+
444
+ private async doFetch(url: string, init: RequestInit): Promise<Response | UndiciResponse> {
445
+ if (this.provider === 'local' && this.fetchImpl === fetch) {
446
+ const uinit: UndiciRequestInit = {
447
+ method: init.method,
448
+ headers: init.headers as UndiciRequestInit['headers'],
449
+ body: init.body as UndiciRequestInit['body'],
450
+ dispatcher: ImageClient.getLocalDispatcher(),
451
+ };
452
+ return undiciFetch(url, uinit);
453
+ }
454
+ return this.fetchImpl(url, init);
334
455
  }
335
456
  }
336
457
 
@@ -340,4 +461,9 @@ export const IMAGE_PROVIDER_CATALOG: ReadonlyArray<{ provider: ImageProvider; mo
340
461
  models: Object.values(OPENROUTER_IMAGE_MODELS),
341
462
  defaultModel: OPENROUTER_IMAGE_DEFAULTS.sheet,
342
463
  },
464
+ {
465
+ provider: 'local',
466
+ models: Object.values(LOCAL_IMAGE_MODELS),
467
+ defaultModel: LOCAL_IMAGE_DEFAULTS.sheet,
468
+ },
343
469
  ];
package/src/index.ts CHANGED
@@ -89,6 +89,8 @@ export {
89
89
  resolveDefaultImageProvider,
90
90
  OPENROUTER_IMAGE_MODELS,
91
91
  OPENROUTER_IMAGE_DEFAULTS,
92
+ LOCAL_IMAGE_MODELS,
93
+ LOCAL_IMAGE_DEFAULTS,
92
94
  IMAGE_PROVIDER_CATALOG,
93
95
  type ImageProvider,
94
96
  type ImageClientOptions,