@almadar/llm 2.47.0 → 2.49.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
@@ -1,10 +1,10 @@
1
1
  import { L as LLMFinishReason, a as LLMClient } from './client-SYMpnw-w.js';
2
2
  export { A as ANTHROPIC_MODELS, C as CacheAwareLLMCallOptions, b as CacheableBlock, c as ChatCompletionChoice, d as ChatCompletionMessage, e as ChatCompletionResponse, f as ChatCompletionRole, g as ChatCompletionToolCall, h as ChatCompletionToolDef, i as ChatCompletionUsage, D as DEEPSEEK_MODELS, K as KIMI_MODELS, j as LLMCallOptions, k as LLMClientOptions, l as LLMProvider, m as LLMResponse, n as LLMStreamChunk, o as LLMStreamOptions, p as LLMUsage, O as OPENAI_MODELS, q as OPENROUTER_MODELS, P as PROVIDER_CATALOG, r as ProviderCatalogEntry, s as ProviderConfig, V as VisionCallOptions, t as VisionImageMediaType, u as VisionImagePart, v as buildVisionMessageContent, w as createAnthropicClient, x as createCreativeClient, y as createDeepSeekClient, z as createFixClient, B as createKimiClient, E as createOpenAIClient, F as createOpenRouterClient, G as createRequirementsClient, H as createZhipuClient, I as getAvailableProvider, J as getSharedLLMClient, M as isProviderAvailable, N as listLmstudioModels, Q as listSelectableProviders, R as parseChatCompletionResponse, S as resetSharedLLMClient } from './client-SYMpnw-w.js';
3
3
  export { a as RateLimiter, R as RateLimiterOptions, b as TokenTracker, T as TokenUsage, g as getGlobalRateLimiter, c as getGlobalTokenTracker, r as resetGlobalRateLimiter, d as resetGlobalTokenTracker } from './rate-limiter-CXaf8aAy.js';
4
+ import { JsonValue, ServiceParams, ServiceContract } from '@almadar/core';
4
5
  export { autoCloseJson, extractJsonFromText, isValidJson, parseJsonResponse, safeParseJson } from './json-parser.js';
5
6
  import { z } from 'zod';
6
7
  export { JsonSchema, STRUCTURED_OUTPUT_MODELS, StructuredGenerationOptions, StructuredGenerationResult, StructuredOutputClient, StructuredOutputOptions, getStructuredOutputClient, isStructuredOutputAvailable, resetStructuredOutputClient } from './structured-output.js';
7
- import { ServiceParams, ServiceContract } from '@almadar/core';
8
8
  export { ErrorPrediction, GFlowNetResult, GoalSpec, MasarError, MasarGenerateOptions, MasarGenerateResult, MasarHealthResult, MasarProvider, MasarProviderOptions, PredictErrorsResult, RankEditsResult, RankedEdit, getMasarProvider, resetMasarProvider } from './providers/index.js';
9
9
  import '@langchain/openai';
10
10
  import '@langchain/anthropic';
@@ -101,6 +101,110 @@ declare class EmbeddingClient {
101
101
  */
102
102
  declare function cosineSimilarity(a: ReadonlyArray<number>, b: ReadonlyArray<number>): number;
103
103
 
104
+ /**
105
+ * Image Client
106
+ *
107
+ * Single-purpose client for OpenRouter's image-generation API
108
+ * (`POST {baseUrl}/images`). Raw `fetch`, no SDK — mirrors
109
+ * `embedding-client.ts`'s shape (provider tables, retry policy, the
110
+ * three response-error surfaces).
111
+ *
112
+ * @packageDocumentation
113
+ */
114
+
115
+ type ImageProvider = 'openrouter';
116
+ /** Canonical image-provider resolution, mirrors `resolveDefaultEmbeddingProvider`. */
117
+ declare function resolveDefaultImageProvider(): ImageProvider;
118
+ declare const OPENROUTER_IMAGE_MODELS: {
119
+ readonly SEEDREAM_5_LITE: "bytedance-seed/seedream-5-0-lite";
120
+ readonly SEEDREAM_4_5: "bytedance-seed/seedream-4.5";
121
+ readonly GEMINI_3_1_FLASH_LITE_IMAGE: "google/gemini-3.1-flash-lite-image";
122
+ readonly GEMINI_2_5_FLASH_IMAGE: "google/gemini-2.5-flash-image";
123
+ readonly FLUX_2_PRO: "black-forest-labs/flux.2-pro";
124
+ };
125
+ declare const OPENROUTER_IMAGE_DEFAULTS: {
126
+ readonly sheet: "bytedance-seed/seedream-5-0-lite";
127
+ readonly preview: "google/gemini-3.1-flash-lite-image";
128
+ };
129
+ interface ImageClientOptions {
130
+ provider?: ImageProvider;
131
+ model?: string;
132
+ apiKey?: string;
133
+ baseUrl?: string;
134
+ fetchImpl?: typeof fetch;
135
+ }
136
+ /** An input reference image — `https:` or `data:` URL. Transport only, no fetching/validation. */
137
+ interface ImageReference {
138
+ url: string;
139
+ }
140
+ interface ImageGenerateOptions {
141
+ prompt: string;
142
+ model?: string;
143
+ n?: number;
144
+ size?: `${number}x${number}`;
145
+ resolution?: '512' | '1K' | '2K' | '4K';
146
+ aspectRatio?: string;
147
+ quality?: 'auto' | 'low' | 'medium' | 'high';
148
+ background?: 'auto' | 'transparent' | 'opaque';
149
+ seed?: number;
150
+ references?: ReadonlyArray<ImageReference>;
151
+ provider?: {
152
+ order?: string[];
153
+ only?: string[];
154
+ allowFallbacks?: boolean;
155
+ };
156
+ }
157
+ interface GeneratedImage {
158
+ bytes: Uint8Array;
159
+ mediaType: string;
160
+ }
161
+ interface ImageGenerateResult {
162
+ images: GeneratedImage[];
163
+ usage: {
164
+ cost: number;
165
+ totalTokens: number;
166
+ };
167
+ model: string;
168
+ dropped: string[];
169
+ }
170
+ interface ImageModelCapabilities {
171
+ model: string;
172
+ supportedParameters: ReadonlySet<string>;
173
+ raw: JsonValue;
174
+ }
175
+ declare class ImageClient {
176
+ private readonly provider;
177
+ private readonly model;
178
+ private readonly apiKey;
179
+ private readonly baseUrl;
180
+ private readonly fetchImpl;
181
+ private readonly capabilitiesCache;
182
+ constructor(opts?: ImageClientOptions);
183
+ /** Generate images. Never strips request fields — `dropped` is always `[]`. */
184
+ generate(opts: ImageGenerateOptions): Promise<ImageGenerateResult>;
185
+ /**
186
+ * Per-model `.../endpoints` capabilities, memoized per instance per
187
+ * model. `supportedParameters` is the empty set (not a throw) when the
188
+ * response shape doesn't match what's expected.
189
+ */
190
+ capabilities(modelId?: string): Promise<ImageModelCapabilities>;
191
+ /**
192
+ * Like `generate`, but first reads `capabilities(model)` and strips
193
+ * any of `background`/`seed`/`references` the model doesn't list in
194
+ * `supported_parameters` — their (OpenRouter) names come back in
195
+ * `dropped` rather than being silently swallowed.
196
+ */
197
+ generateChecked(opts: ImageGenerateOptions): Promise<ImageGenerateResult>;
198
+ private performGenerate;
199
+ /** Retries on network failure or a 5xx status; any other response is returned as-is. */
200
+ private fetchWithRetry;
201
+ }
202
+ declare const IMAGE_PROVIDER_CATALOG: ReadonlyArray<{
203
+ provider: ImageProvider;
204
+ models: string[];
205
+ defaultModel: string;
206
+ }>;
207
+
104
208
  /**
105
209
  * Truncation Detector
106
210
  *
@@ -221,7 +325,10 @@ declare function buildGenericContinuationPrompt(context: string, partialResponse
221
325
  */
222
326
  interface JsonSchemaDefinition extends ServiceParams {
223
327
  type?: string;
224
- properties?: JsonSchemaDefinition;
328
+ /** Keyed by field name — each named field carries its OWN nested schema
329
+ * (real JSON-Schema semantics; a single un-keyed nested schema here would
330
+ * say every field of an object shares one shape). */
331
+ properties?: Record<string, JsonSchemaDefinition>;
225
332
  required?: string[];
226
333
  items?: JsonSchemaDefinition;
227
334
  additionalProperties?: JsonSchemaDefinition;
@@ -241,10 +348,15 @@ type LLMServiceActions = {
241
348
  /** Generate text from a prompt. */
242
349
  generate: {
243
350
  params: {
351
+ /** System prompt steering the model's behavior. */
244
352
  systemPrompt?: string;
353
+ /** The user/task prompt sent to the model. */
245
354
  userPrompt: string;
355
+ /** Model id override. */
246
356
  model?: string;
357
+ /** Sampling temperature; higher is more random. */
247
358
  temperature?: number;
359
+ /** Maximum tokens to generate. */
248
360
  maxTokens?: number;
249
361
  };
250
362
  result: {
@@ -259,8 +371,11 @@ type LLMServiceActions = {
259
371
  /** Classify text into one of the given categories. */
260
372
  classify: {
261
373
  params: {
374
+ /** Text to classify. */
262
375
  text: string;
376
+ /** Closed set of category labels the model must choose from. */
263
377
  categories: string[];
378
+ /** Model id override. */
264
379
  model?: string;
265
380
  };
266
381
  result: {
@@ -269,14 +384,22 @@ type LLMServiceActions = {
269
384
  reasoning: string;
270
385
  };
271
386
  };
272
- /** Extract structured data from text according to a JSON schema. */
387
+ /**
388
+ * Extract structured data from text according to a JSON schema.
389
+ * @synonyms parse, structure
390
+ */
273
391
  extract: {
274
392
  params: {
393
+ /** Source text to extract data from. */
275
394
  text: string;
395
+ /** JSON Schema describing the fields to extract. */
276
396
  schema: JsonSchemaDefinition;
397
+ /** Model id override. */
277
398
  model?: string;
278
399
  };
279
400
  result: {
401
+ /** The extracted structure follows the caller's `schema` — the consumer declares its shape.
402
+ * @shapedBy schema */
280
403
  data: ExtractedData;
281
404
  confidence: number;
282
405
  };
@@ -284,9 +407,13 @@ type LLMServiceActions = {
284
407
  /** Summarize text with optional length and style constraints. */
285
408
  summarize: {
286
409
  params: {
410
+ /** Text to summarize. */
287
411
  text: string;
412
+ /** Target maximum summary length in words. */
288
413
  maxLength?: number;
414
+ /** Summary style, e.g. 'bullet' or 'detailed'. */
289
415
  style?: string;
416
+ /** Model id override. */
290
417
  model?: string;
291
418
  };
292
419
  result: {
@@ -297,7 +424,9 @@ type LLMServiceActions = {
297
424
  /** Generate embeddings for an array of texts. */
298
425
  embed: {
299
426
  params: {
427
+ /** Texts to embed, one vector per entry, in order. */
300
428
  texts: string[];
429
+ /** Embedding model id override. */
301
430
  model?: string;
302
431
  };
303
432
  result: {
@@ -312,4 +441,4 @@ type LLMServiceActions = {
312
441
  */
313
442
  type LLMServiceContract = ServiceContract<LLMServiceActions>;
314
443
 
315
- export { type ContinuationOptions, type ContinuationResult, type EmbeddingBatchResult, EmbeddingClient, type EmbeddingClientOptions, type EmbeddingProvider, type EmbeddingResult, type EmbeddingUsage, LLMClient, LLMFinishReason, type LLMServiceActions, type LLMServiceContract, type TruncationReason, type TruncationResult, buildGenericContinuationPrompt, callWithContinuation, cosineSimilarity, detectTruncation, findLastCompleteElement, isLikelyTruncated, mergeResponses, resolveDefaultEmbeddingProvider, salvagePartialResponse };
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 };
package/dist/index.js CHANGED
@@ -198,6 +198,239 @@ function cosineSimilarity(a, b) {
198
198
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
199
199
  }
200
200
 
201
+ // src/image-client.ts
202
+ var IMAGE_PROVIDERS = ["openrouter"];
203
+ function resolveDefaultImageProvider() {
204
+ const v = process.env.IMAGE_PROVIDER;
205
+ return v && IMAGE_PROVIDERS.includes(v) ? v : "openrouter";
206
+ }
207
+ var OPENROUTER_IMAGE_MODELS = {
208
+ SEEDREAM_5_LITE: "bytedance-seed/seedream-5-0-lite",
209
+ SEEDREAM_4_5: "bytedance-seed/seedream-4.5",
210
+ GEMINI_3_1_FLASH_LITE_IMAGE: "google/gemini-3.1-flash-lite-image",
211
+ GEMINI_2_5_FLASH_IMAGE: "google/gemini-2.5-flash-image",
212
+ FLUX_2_PRO: "black-forest-labs/flux.2-pro"
213
+ };
214
+ var OPENROUTER_IMAGE_DEFAULTS = {
215
+ sheet: OPENROUTER_IMAGE_MODELS.SEEDREAM_5_LITE,
216
+ preview: OPENROUTER_IMAGE_MODELS.GEMINI_3_1_FLASH_LITE_IMAGE
217
+ };
218
+ var IMAGE_MAX_ATTEMPTS = 3;
219
+ var IMAGE_RETRY_BACKOFF_MS = [500, 2e3];
220
+ var DEFAULT_MODELS2 = {
221
+ openrouter: OPENROUTER_IMAGE_DEFAULTS.sheet
222
+ };
223
+ var DEFAULT_BASE_URLS2 = {
224
+ openrouter: "https://openrouter.ai/api/v1"
225
+ };
226
+ var API_KEY_ENV_VARS2 = {
227
+ openrouter: "OPEN_ROUTER_API_KEY"
228
+ };
229
+ var IMAGE_CHECKED_PARAM_NAMES = {
230
+ background: "background",
231
+ seed: "seed",
232
+ references: "input_references"
233
+ };
234
+ function isJsonObject(value) {
235
+ return typeof value === "object" && value !== null && !Array.isArray(value);
236
+ }
237
+ function asString(value) {
238
+ return typeof value === "string" ? value : void 0;
239
+ }
240
+ function asNumber(value) {
241
+ return typeof value === "number" ? value : void 0;
242
+ }
243
+ function parseJsonBody(provider, rawBody) {
244
+ try {
245
+ return JSON.parse(rawBody);
246
+ } catch {
247
+ throw new Error(`ImageClient: ${provider} returned unparseable body: ${rawBody}`);
248
+ }
249
+ }
250
+ function decodeBase64(b64) {
251
+ if (typeof globalThis.Buffer !== "undefined") {
252
+ return new Uint8Array(globalThis.Buffer.from(b64, "base64"));
253
+ }
254
+ const binary = atob(b64);
255
+ const bytes = new Uint8Array(binary.length);
256
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
257
+ return bytes;
258
+ }
259
+ function collectSupportedParameters(body) {
260
+ const params = /* @__PURE__ */ new Set();
261
+ if (!isJsonObject(body)) return params;
262
+ const candidateArrays = [];
263
+ if (Array.isArray(body.data)) candidateArrays.push(body.data);
264
+ if (Array.isArray(body.endpoints)) candidateArrays.push(body.endpoints);
265
+ for (const arr of candidateArrays) {
266
+ for (const entry of arr) {
267
+ if (!isJsonObject(entry)) continue;
268
+ const sp = entry.supported_parameters;
269
+ if (!Array.isArray(sp)) continue;
270
+ for (const p of sp) if (typeof p === "string") params.add(p);
271
+ }
272
+ }
273
+ return params;
274
+ }
275
+ var ImageClient = class {
276
+ constructor(opts = {}) {
277
+ this.capabilitiesCache = /* @__PURE__ */ new Map();
278
+ this.provider = opts.provider ?? "openrouter";
279
+ this.model = opts.model ?? DEFAULT_MODELS2[this.provider];
280
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URLS2[this.provider]).replace(/\/$/, "");
281
+ this.fetchImpl = opts.fetchImpl ?? fetch;
282
+ const apiKey = opts.apiKey ?? process.env[API_KEY_ENV_VARS2[this.provider]];
283
+ if (!apiKey) {
284
+ throw new Error(
285
+ `ImageClient: API key for provider '${this.provider}' not found. Set ${API_KEY_ENV_VARS2[this.provider]} in your environment or pass options.apiKey.`
286
+ );
287
+ }
288
+ this.apiKey = apiKey;
289
+ }
290
+ /** Generate images. Never strips request fields — `dropped` is always `[]`. */
291
+ async generate(opts) {
292
+ return this.performGenerate(opts, []);
293
+ }
294
+ /**
295
+ * Per-model `.../endpoints` capabilities, memoized per instance per
296
+ * model. `supportedParameters` is the empty set (not a throw) when the
297
+ * response shape doesn't match what's expected.
298
+ */
299
+ async capabilities(modelId) {
300
+ const model = modelId ?? this.model;
301
+ const cached = this.capabilitiesCache.get(model);
302
+ if (cached) return cached;
303
+ const response = await this.fetchWithRetry(`${this.baseUrl}/images/models/${model}/endpoints`, {
304
+ method: "GET",
305
+ headers: { Authorization: `Bearer ${this.apiKey}` }
306
+ });
307
+ if (!response.ok) {
308
+ const errorText = await response.text().catch(() => "<no body>");
309
+ throw new Error(
310
+ `ImageClient: ${this.provider} returned ${response.status} ${response.statusText}: ${errorText}`
311
+ );
312
+ }
313
+ const body = parseJsonBody(this.provider, await response.text());
314
+ const result = { model, supportedParameters: collectSupportedParameters(body), raw: body };
315
+ this.capabilitiesCache.set(model, result);
316
+ return result;
317
+ }
318
+ /**
319
+ * Like `generate`, but first reads `capabilities(model)` and strips
320
+ * any of `background`/`seed`/`references` the model doesn't list in
321
+ * `supported_parameters` — their (OpenRouter) names come back in
322
+ * `dropped` rather than being silently swallowed.
323
+ */
324
+ async generateChecked(opts) {
325
+ const model = opts.model ?? this.model;
326
+ const caps = await this.capabilities(model);
327
+ const dropped = [];
328
+ const filtered = { ...opts, model };
329
+ if (filtered.background !== void 0 && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.background)) {
330
+ delete filtered.background;
331
+ dropped.push(IMAGE_CHECKED_PARAM_NAMES.background);
332
+ }
333
+ if (filtered.seed !== void 0 && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.seed)) {
334
+ delete filtered.seed;
335
+ dropped.push(IMAGE_CHECKED_PARAM_NAMES.seed);
336
+ }
337
+ if (filtered.references !== void 0 && filtered.references.length > 0 && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.references)) {
338
+ delete filtered.references;
339
+ dropped.push(IMAGE_CHECKED_PARAM_NAMES.references);
340
+ }
341
+ return this.performGenerate(filtered, dropped);
342
+ }
343
+ async performGenerate(opts, dropped) {
344
+ if (opts.size !== void 0 && (opts.resolution !== void 0 || opts.aspectRatio !== void 0)) {
345
+ throw new Error("ImageClient.generate: `size` cannot be combined with `resolution`/`aspectRatio` \u2014 pass one or the other");
346
+ }
347
+ const model = opts.model ?? this.model;
348
+ const requestBody = { model, prompt: opts.prompt, output_format: "png" };
349
+ if (opts.n !== void 0) requestBody.n = opts.n;
350
+ if (opts.size !== void 0) requestBody.size = opts.size;
351
+ if (opts.resolution !== void 0) requestBody.resolution = opts.resolution;
352
+ if (opts.aspectRatio !== void 0) requestBody.aspect_ratio = opts.aspectRatio;
353
+ if (opts.quality !== void 0) requestBody.quality = opts.quality;
354
+ if (opts.background !== void 0) requestBody.background = opts.background;
355
+ if (opts.seed !== void 0) requestBody.seed = opts.seed;
356
+ if (opts.references !== void 0 && opts.references.length > 0) {
357
+ requestBody.input_references = opts.references.map((r) => ({ type: "image_url", image_url: { url: r.url } }));
358
+ }
359
+ if (opts.provider !== void 0) {
360
+ const providerField = {};
361
+ if (opts.provider.order !== void 0) providerField.order = opts.provider.order;
362
+ if (opts.provider.only !== void 0) providerField.only = opts.provider.only;
363
+ if (opts.provider.allowFallbacks !== void 0) providerField.allow_fallbacks = opts.provider.allowFallbacks;
364
+ requestBody.provider = providerField;
365
+ }
366
+ const response = await this.fetchWithRetry(`${this.baseUrl}/images`, {
367
+ method: "POST",
368
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` },
369
+ body: JSON.stringify(requestBody)
370
+ });
371
+ if (!response.ok) {
372
+ const errorText = await response.text().catch(() => "<no body>");
373
+ throw new Error(
374
+ `ImageClient: ${this.provider} returned ${response.status} ${response.statusText}: ${errorText}`
375
+ );
376
+ }
377
+ const rawBody = await response.text();
378
+ const body = parseJsonBody(this.provider, rawBody);
379
+ if (!isJsonObject(body)) {
380
+ throw new Error(`ImageClient: ${this.provider} returned unparseable body: ${rawBody}`);
381
+ }
382
+ if (isJsonObject(body.error)) {
383
+ const detail = asString(body.error.message) ?? rawBody;
384
+ throw new Error(`ImageClient: ${this.provider} returned an error: ${detail}`);
385
+ }
386
+ if (!Array.isArray(body.data)) {
387
+ throw new Error(`ImageClient: ${this.provider} response has no \`data\` array: ${rawBody}`);
388
+ }
389
+ const images = body.data.map((entry, i) => {
390
+ if (!isJsonObject(entry)) {
391
+ throw new Error(`ImageClient: ${this.provider} response data[${i}] is not an object: ${rawBody}`);
392
+ }
393
+ const b64 = asString(entry.b64_json);
394
+ if (!b64) {
395
+ throw new Error(`ImageClient: ${this.provider} response data[${i}] missing \`b64_json\`: ${rawBody}`);
396
+ }
397
+ return { bytes: decodeBase64(b64), mediaType: asString(entry.media_type) ?? "image/png" };
398
+ });
399
+ const usageObj = isJsonObject(body.usage) ? body.usage : void 0;
400
+ const usage = {
401
+ cost: asNumber(usageObj?.cost) ?? 0,
402
+ totalTokens: asNumber(usageObj?.total_tokens) ?? 0
403
+ };
404
+ return { images, usage, model, dropped };
405
+ }
406
+ /** Retries on network failure or a 5xx status; any other response is returned as-is. */
407
+ async fetchWithRetry(url, init) {
408
+ let lastNetworkError;
409
+ let response;
410
+ for (let attempt = 1; attempt <= IMAGE_MAX_ATTEMPTS; attempt++) {
411
+ try {
412
+ response = await this.fetchImpl(url, init);
413
+ } catch (e) {
414
+ response = void 0;
415
+ lastNetworkError = e instanceof Error ? e : new Error(String(e));
416
+ }
417
+ if (response !== void 0 && response.status < 500) return response;
418
+ const backoff = IMAGE_RETRY_BACKOFF_MS[attempt - 1];
419
+ if (backoff === void 0) break;
420
+ await new Promise((r) => setTimeout(r, backoff));
421
+ }
422
+ if (response) return response;
423
+ throw lastNetworkError ?? new Error(`ImageClient: ${this.provider} request failed after ${IMAGE_MAX_ATTEMPTS} attempts`);
424
+ }
425
+ };
426
+ var IMAGE_PROVIDER_CATALOG = [
427
+ {
428
+ provider: "openrouter",
429
+ models: Object.values(OPENROUTER_IMAGE_MODELS),
430
+ defaultModel: OPENROUTER_IMAGE_DEFAULTS.sheet
431
+ }
432
+ ];
433
+
201
434
  // src/truncation-detector.ts
202
435
  function detectTruncation(response, finishReason) {
203
436
  if (finishReason === "length") {
@@ -596,11 +829,15 @@ export {
596
829
  ANTHROPIC_MODELS,
597
830
  DEEPSEEK_MODELS,
598
831
  EmbeddingClient,
832
+ IMAGE_PROVIDER_CATALOG,
833
+ ImageClient,
599
834
  KIMI_MODELS,
600
835
  LLMClient,
601
836
  MasarError,
602
837
  MasarProvider,
603
838
  OPENAI_MODELS,
839
+ OPENROUTER_IMAGE_DEFAULTS,
840
+ OPENROUTER_IMAGE_MODELS,
604
841
  OPENROUTER_MODELS,
605
842
  PROVIDER_CATALOG,
606
843
  RateLimiter,
@@ -645,6 +882,7 @@ export {
645
882
  resetSharedLLMClient,
646
883
  resetStructuredOutputClient,
647
884
  resolveDefaultEmbeddingProvider,
885
+ resolveDefaultImageProvider,
648
886
  safeParseJson,
649
887
  salvagePartialResponse
650
888
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/embedding-client.ts","../src/truncation-detector.ts","../src/continuation.ts"],"sourcesContent":["/**\n * Embedding Client\n *\n * Single-purpose client for text embeddings. Used by `@almadar-io/agent`'s\n * cosine-similarity catalog retrieval (rank organisms + atoms against the\n * user's request before rendering Stage A's prompt) and by\n * `@almadar/std`'s publish-time embedding bake step.\n *\n * Providers:\n * - `openai` (default model `text-embedding-3-small`, 1536-d) — requires\n * `OPENAI_API_KEY`.\n * - `openrouter` (default model `baai/bge-base-en-v1.5`, 768-d) — requires\n * `OPEN_ROUTER_API_KEY`. Same OpenAI-compatible request shape, just a\n * different base URL.\n * - `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 * 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;;;ACxNO,SAAS,iBACd,UACA,cACkB;AAClB,MAAI,iBAAiB,UAAU;AAC7B,UAAMA,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":["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 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":"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","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.47.0",
3
+ "version": "2.49.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,11 +47,11 @@
47
47
  "zod": "^3.22.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "@almadar/core": "^10.76.0"
50
+ "@almadar/core": "^10.87.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@almadar/core": "^10.76.0",
54
- "@almadar/eslint-plugin": "^2.17.0",
53
+ "@almadar/core": "^10.87.0",
54
+ "@almadar/eslint-plugin": "^2.18.0",
55
55
  "@types/node": "^22.0.0",
56
56
  "@typescript-eslint/parser": "8.65.0",
57
57
  "@typescript/native": "npm:typescript@^7.0.2",
package/src/contracts.ts CHANGED
@@ -21,7 +21,10 @@ import type { ServiceContract, ServiceParams, ServiceParamsValue } from "@almada
21
21
  */
22
22
  interface JsonSchemaDefinition extends ServiceParams {
23
23
  type?: string;
24
- properties?: JsonSchemaDefinition;
24
+ /** Keyed by field name — each named field carries its OWN nested schema
25
+ * (real JSON-Schema semantics; a single un-keyed nested schema here would
26
+ * say every field of an object shares one shape). */
27
+ properties?: Record<string, JsonSchemaDefinition>;
25
28
  required?: string[];
26
29
  items?: JsonSchemaDefinition;
27
30
  additionalProperties?: JsonSchemaDefinition;
@@ -41,10 +44,15 @@ export type LLMServiceActions = {
41
44
  /** Generate text from a prompt. */
42
45
  generate: {
43
46
  params: {
47
+ /** System prompt steering the model's behavior. */
44
48
  systemPrompt?: string;
49
+ /** The user/task prompt sent to the model. */
45
50
  userPrompt: string;
51
+ /** Model id override. */
46
52
  model?: string;
53
+ /** Sampling temperature; higher is more random. */
47
54
  temperature?: number;
55
+ /** Maximum tokens to generate. */
48
56
  maxTokens?: number;
49
57
  };
50
58
  result: {
@@ -60,8 +68,11 @@ export type LLMServiceActions = {
60
68
  /** Classify text into one of the given categories. */
61
69
  classify: {
62
70
  params: {
71
+ /** Text to classify. */
63
72
  text: string;
73
+ /** Closed set of category labels the model must choose from. */
64
74
  categories: string[];
75
+ /** Model id override. */
65
76
  model?: string;
66
77
  };
67
78
  result: {
@@ -71,14 +82,22 @@ export type LLMServiceActions = {
71
82
  };
72
83
  };
73
84
 
74
- /** Extract structured data from text according to a JSON schema. */
85
+ /**
86
+ * Extract structured data from text according to a JSON schema.
87
+ * @synonyms parse, structure
88
+ */
75
89
  extract: {
76
90
  params: {
91
+ /** Source text to extract data from. */
77
92
  text: string;
93
+ /** JSON Schema describing the fields to extract. */
78
94
  schema: JsonSchemaDefinition;
95
+ /** Model id override. */
79
96
  model?: string;
80
97
  };
81
98
  result: {
99
+ /** The extracted structure follows the caller's `schema` — the consumer declares its shape.
100
+ * @shapedBy schema */
82
101
  data: ExtractedData;
83
102
  confidence: number;
84
103
  };
@@ -87,9 +106,13 @@ export type LLMServiceActions = {
87
106
  /** Summarize text with optional length and style constraints. */
88
107
  summarize: {
89
108
  params: {
109
+ /** Text to summarize. */
90
110
  text: string;
111
+ /** Target maximum summary length in words. */
91
112
  maxLength?: number;
113
+ /** Summary style, e.g. 'bullet' or 'detailed'. */
92
114
  style?: string;
115
+ /** Model id override. */
93
116
  model?: string;
94
117
  };
95
118
  result: {
@@ -101,7 +124,9 @@ export type LLMServiceActions = {
101
124
  /** Generate embeddings for an array of texts. */
102
125
  embed: {
103
126
  params: {
127
+ /** Texts to embed, one vector per entry, in order. */
104
128
  texts: string[];
129
+ /** Embedding model id override. */
105
130
  model?: string;
106
131
  };
107
132
  result: {
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Image Client
3
+ *
4
+ * Single-purpose client for OpenRouter's image-generation API
5
+ * (`POST {baseUrl}/images`). Raw `fetch`, no SDK — mirrors
6
+ * `embedding-client.ts`'s shape (provider tables, retry policy, the
7
+ * three response-error surfaces).
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+
12
+ import type { JsonValue } from '@almadar/core';
13
+
14
+ export type ImageProvider = 'openrouter';
15
+
16
+ const IMAGE_PROVIDERS: readonly ImageProvider[] = ['openrouter'];
17
+
18
+ /** Canonical image-provider resolution, mirrors `resolveDefaultEmbeddingProvider`. */
19
+ export function resolveDefaultImageProvider(): ImageProvider {
20
+ const v = process.env.IMAGE_PROVIDER;
21
+ return v && (IMAGE_PROVIDERS as readonly string[]).includes(v) ? (v as ImageProvider) : 'openrouter';
22
+ }
23
+
24
+ export const OPENROUTER_IMAGE_MODELS = {
25
+ SEEDREAM_5_LITE: 'bytedance-seed/seedream-5-0-lite',
26
+ SEEDREAM_4_5: 'bytedance-seed/seedream-4.5',
27
+ GEMINI_3_1_FLASH_LITE_IMAGE: 'google/gemini-3.1-flash-lite-image',
28
+ GEMINI_2_5_FLASH_IMAGE: 'google/gemini-2.5-flash-image',
29
+ FLUX_2_PRO: 'black-forest-labs/flux.2-pro',
30
+ } as const;
31
+
32
+ export const OPENROUTER_IMAGE_DEFAULTS = {
33
+ sheet: OPENROUTER_IMAGE_MODELS.SEEDREAM_5_LITE,
34
+ preview: OPENROUTER_IMAGE_MODELS.GEMINI_3_1_FLASH_LITE_IMAGE,
35
+ } as const;
36
+
37
+ export interface ImageClientOptions {
38
+ provider?: ImageProvider;
39
+ model?: string;
40
+ apiKey?: string;
41
+ baseUrl?: string;
42
+ fetchImpl?: typeof fetch;
43
+ }
44
+
45
+ /** An input reference image — `https:` or `data:` URL. Transport only, no fetching/validation. */
46
+ export interface ImageReference {
47
+ url: string;
48
+ }
49
+
50
+ export interface ImageGenerateOptions {
51
+ prompt: string;
52
+ model?: string;
53
+ n?: number;
54
+ size?: `${number}x${number}`;
55
+ resolution?: '512' | '1K' | '2K' | '4K';
56
+ aspectRatio?: string;
57
+ quality?: 'auto' | 'low' | 'medium' | 'high';
58
+ background?: 'auto' | 'transparent' | 'opaque';
59
+ seed?: number;
60
+ references?: ReadonlyArray<ImageReference>;
61
+ provider?: { order?: string[]; only?: string[]; allowFallbacks?: boolean };
62
+ }
63
+
64
+ export interface GeneratedImage {
65
+ bytes: Uint8Array;
66
+ mediaType: string;
67
+ }
68
+
69
+ export interface ImageGenerateResult {
70
+ images: GeneratedImage[];
71
+ usage: { cost: number; totalTokens: number };
72
+ model: string;
73
+ dropped: string[];
74
+ }
75
+
76
+ export interface ImageModelCapabilities {
77
+ model: string;
78
+ supportedParameters: ReadonlySet<string>;
79
+ raw: JsonValue;
80
+ }
81
+
82
+ /** Transient-failure retry policy: attempts and backoff (ms) between them. */
83
+ const IMAGE_MAX_ATTEMPTS = 3;
84
+ const IMAGE_RETRY_BACKOFF_MS = [500, 2000];
85
+
86
+ const DEFAULT_MODELS: Record<ImageProvider, string> = {
87
+ openrouter: OPENROUTER_IMAGE_DEFAULTS.sheet,
88
+ };
89
+
90
+ const DEFAULT_BASE_URLS: Record<ImageProvider, string> = {
91
+ openrouter: 'https://openrouter.ai/api/v1',
92
+ };
93
+
94
+ const API_KEY_ENV_VARS: Record<ImageProvider, string> = {
95
+ openrouter: 'OPEN_ROUTER_API_KEY',
96
+ };
97
+
98
+ /**
99
+ * Request field → OpenRouter `supported_parameters` name. The single
100
+ * place `generateChecked` reads to decide what to strip — change the
101
+ * mapping here, not at each call site.
102
+ */
103
+ const IMAGE_CHECKED_PARAM_NAMES = {
104
+ background: 'background',
105
+ seed: 'seed',
106
+ references: 'input_references',
107
+ } as const;
108
+
109
+ function isJsonObject(value: JsonValue): value is { [key: string]: JsonValue } {
110
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
111
+ }
112
+
113
+ function asString(value: JsonValue | undefined): string | undefined {
114
+ return typeof value === 'string' ? value : undefined;
115
+ }
116
+
117
+ function asNumber(value: JsonValue | undefined): number | undefined {
118
+ return typeof value === 'number' ? value : undefined;
119
+ }
120
+
121
+ function parseJsonBody(provider: ImageProvider, rawBody: string): JsonValue {
122
+ try {
123
+ return JSON.parse(rawBody) as JsonValue;
124
+ } catch {
125
+ throw new Error(`ImageClient: ${provider} returned unparseable body: ${rawBody}`);
126
+ }
127
+ }
128
+
129
+ function decodeBase64(b64: string): Uint8Array {
130
+ if (typeof globalThis.Buffer !== 'undefined') {
131
+ return new Uint8Array(globalThis.Buffer.from(b64, 'base64'));
132
+ }
133
+ const binary = atob(b64);
134
+ const bytes = new Uint8Array(binary.length);
135
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
136
+ return bytes;
137
+ }
138
+
139
+ /**
140
+ * Collects the union of `supported_parameters` across a `.../endpoints`
141
+ * response's `data`/`endpoints` arrays. Returns an empty set (never
142
+ * throws) when the shape doesn't match — the caller still gets `raw`.
143
+ */
144
+ function collectSupportedParameters(body: JsonValue): ReadonlySet<string> {
145
+ const params = new Set<string>();
146
+ if (!isJsonObject(body)) return params;
147
+ const candidateArrays: JsonValue[][] = [];
148
+ if (Array.isArray(body.data)) candidateArrays.push(body.data);
149
+ if (Array.isArray(body.endpoints)) candidateArrays.push(body.endpoints);
150
+ for (const arr of candidateArrays) {
151
+ for (const entry of arr) {
152
+ if (!isJsonObject(entry)) continue;
153
+ const sp = entry.supported_parameters;
154
+ if (!Array.isArray(sp)) continue;
155
+ for (const p of sp) if (typeof p === 'string') params.add(p);
156
+ }
157
+ }
158
+ return params;
159
+ }
160
+
161
+ export class ImageClient {
162
+ private readonly provider: ImageProvider;
163
+ private readonly model: string;
164
+ private readonly apiKey: string;
165
+ private readonly baseUrl: string;
166
+ private readonly fetchImpl: typeof fetch;
167
+ private readonly capabilitiesCache = new Map<string, ImageModelCapabilities>();
168
+
169
+ constructor(opts: ImageClientOptions = {}) {
170
+ this.provider = opts.provider ?? 'openrouter';
171
+ this.model = opts.model ?? DEFAULT_MODELS[this.provider];
172
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URLS[this.provider]).replace(/\/$/, '');
173
+ this.fetchImpl = opts.fetchImpl ?? fetch;
174
+
175
+ const apiKey = opts.apiKey ?? process.env[API_KEY_ENV_VARS[this.provider]];
176
+ if (!apiKey) {
177
+ throw new Error(
178
+ `ImageClient: API key for provider '${this.provider}' not found. ` +
179
+ `Set ${API_KEY_ENV_VARS[this.provider]} in your environment or pass options.apiKey.`,
180
+ );
181
+ }
182
+ this.apiKey = apiKey;
183
+ }
184
+
185
+ /** Generate images. Never strips request fields — `dropped` is always `[]`. */
186
+ async generate(opts: ImageGenerateOptions): Promise<ImageGenerateResult> {
187
+ return this.performGenerate(opts, []);
188
+ }
189
+
190
+ /**
191
+ * Per-model `.../endpoints` capabilities, memoized per instance per
192
+ * model. `supportedParameters` is the empty set (not a throw) when the
193
+ * response shape doesn't match what's expected.
194
+ */
195
+ async capabilities(modelId?: string): Promise<ImageModelCapabilities> {
196
+ const model = modelId ?? this.model;
197
+ const cached = this.capabilitiesCache.get(model);
198
+ if (cached) return cached;
199
+
200
+ const response = await this.fetchWithRetry(`${this.baseUrl}/images/models/${model}/endpoints`, {
201
+ method: 'GET',
202
+ headers: { Authorization: `Bearer ${this.apiKey}` },
203
+ });
204
+ if (!response.ok) {
205
+ const errorText = await response.text().catch(() => '<no body>');
206
+ throw new Error(
207
+ `ImageClient: ${this.provider} returned ${response.status} ${response.statusText}: ${errorText}`,
208
+ );
209
+ }
210
+ const body = parseJsonBody(this.provider, await response.text());
211
+ const result: ImageModelCapabilities = { model, supportedParameters: collectSupportedParameters(body), raw: body };
212
+ this.capabilitiesCache.set(model, result);
213
+ return result;
214
+ }
215
+
216
+ /**
217
+ * Like `generate`, but first reads `capabilities(model)` and strips
218
+ * any of `background`/`seed`/`references` the model doesn't list in
219
+ * `supported_parameters` — their (OpenRouter) names come back in
220
+ * `dropped` rather than being silently swallowed.
221
+ */
222
+ async generateChecked(opts: ImageGenerateOptions): Promise<ImageGenerateResult> {
223
+ const model = opts.model ?? this.model;
224
+ const caps = await this.capabilities(model);
225
+ const dropped: string[] = [];
226
+ const filtered: ImageGenerateOptions = { ...opts, model };
227
+
228
+ if (filtered.background !== undefined && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.background)) {
229
+ delete filtered.background;
230
+ dropped.push(IMAGE_CHECKED_PARAM_NAMES.background);
231
+ }
232
+ if (filtered.seed !== undefined && !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.seed)) {
233
+ delete filtered.seed;
234
+ dropped.push(IMAGE_CHECKED_PARAM_NAMES.seed);
235
+ }
236
+ if (
237
+ filtered.references !== undefined &&
238
+ filtered.references.length > 0 &&
239
+ !caps.supportedParameters.has(IMAGE_CHECKED_PARAM_NAMES.references)
240
+ ) {
241
+ delete filtered.references;
242
+ dropped.push(IMAGE_CHECKED_PARAM_NAMES.references);
243
+ }
244
+
245
+ return this.performGenerate(filtered, dropped);
246
+ }
247
+
248
+ private async performGenerate(opts: ImageGenerateOptions, dropped: string[]): Promise<ImageGenerateResult> {
249
+ if (opts.size !== undefined && (opts.resolution !== undefined || opts.aspectRatio !== undefined)) {
250
+ throw new Error('ImageClient.generate: `size` cannot be combined with `resolution`/`aspectRatio` — pass one or the other');
251
+ }
252
+ const model = opts.model ?? this.model;
253
+ const requestBody: Record<string, JsonValue> = { model, prompt: opts.prompt, output_format: 'png' };
254
+ if (opts.n !== undefined) requestBody.n = opts.n;
255
+ if (opts.size !== undefined) requestBody.size = opts.size;
256
+ if (opts.resolution !== undefined) requestBody.resolution = opts.resolution;
257
+ if (opts.aspectRatio !== undefined) requestBody.aspect_ratio = opts.aspectRatio;
258
+ if (opts.quality !== undefined) requestBody.quality = opts.quality;
259
+ if (opts.background !== undefined) requestBody.background = opts.background;
260
+ if (opts.seed !== undefined) requestBody.seed = opts.seed;
261
+ if (opts.references !== undefined && opts.references.length > 0) {
262
+ requestBody.input_references = opts.references.map((r) => ({ type: 'image_url', image_url: { url: r.url } }));
263
+ }
264
+ if (opts.provider !== undefined) {
265
+ const providerField: Record<string, JsonValue> = {};
266
+ if (opts.provider.order !== undefined) providerField.order = opts.provider.order;
267
+ if (opts.provider.only !== undefined) providerField.only = opts.provider.only;
268
+ if (opts.provider.allowFallbacks !== undefined) providerField.allow_fallbacks = opts.provider.allowFallbacks;
269
+ requestBody.provider = providerField;
270
+ }
271
+
272
+ const response = await this.fetchWithRetry(`${this.baseUrl}/images`, {
273
+ method: 'POST',
274
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.apiKey}` },
275
+ body: JSON.stringify(requestBody),
276
+ });
277
+ if (!response.ok) {
278
+ const errorText = await response.text().catch(() => '<no body>');
279
+ throw new Error(
280
+ `ImageClient: ${this.provider} returned ${response.status} ${response.statusText}: ${errorText}`,
281
+ );
282
+ }
283
+ const rawBody = await response.text();
284
+ const body = parseJsonBody(this.provider, rawBody);
285
+ if (!isJsonObject(body)) {
286
+ throw new Error(`ImageClient: ${this.provider} returned unparseable body: ${rawBody}`);
287
+ }
288
+ if (isJsonObject(body.error)) {
289
+ const detail = asString(body.error.message) ?? rawBody;
290
+ throw new Error(`ImageClient: ${this.provider} returned an error: ${detail}`);
291
+ }
292
+ if (!Array.isArray(body.data)) {
293
+ throw new Error(`ImageClient: ${this.provider} response has no \`data\` array: ${rawBody}`);
294
+ }
295
+
296
+ const images: GeneratedImage[] = body.data.map((entry, i) => {
297
+ if (!isJsonObject(entry)) {
298
+ throw new Error(`ImageClient: ${this.provider} response data[${i}] is not an object: ${rawBody}`);
299
+ }
300
+ const b64 = asString(entry.b64_json);
301
+ if (!b64) {
302
+ throw new Error(`ImageClient: ${this.provider} response data[${i}] missing \`b64_json\`: ${rawBody}`);
303
+ }
304
+ return { bytes: decodeBase64(b64), mediaType: asString(entry.media_type) ?? 'image/png' };
305
+ });
306
+
307
+ const usageObj = isJsonObject(body.usage) ? body.usage : undefined;
308
+ const usage = {
309
+ cost: asNumber(usageObj?.cost) ?? 0,
310
+ totalTokens: asNumber(usageObj?.total_tokens) ?? 0,
311
+ };
312
+
313
+ return { images, usage, model, dropped };
314
+ }
315
+
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> {
318
+ let lastNetworkError: Error | undefined;
319
+ let response: Response | undefined;
320
+ for (let attempt = 1; attempt <= IMAGE_MAX_ATTEMPTS; attempt++) {
321
+ try {
322
+ response = await this.fetchImpl(url, init);
323
+ } catch (e) {
324
+ response = undefined;
325
+ lastNetworkError = e instanceof Error ? e : new Error(String(e));
326
+ }
327
+ if (response !== undefined && response.status < 500) return response;
328
+ const backoff = IMAGE_RETRY_BACKOFF_MS[attempt - 1];
329
+ if (backoff === undefined) break;
330
+ await new Promise((r) => setTimeout(r, backoff));
331
+ }
332
+ if (response) return response;
333
+ throw lastNetworkError ?? new Error(`ImageClient: ${this.provider} request failed after ${IMAGE_MAX_ATTEMPTS} attempts`);
334
+ }
335
+ }
336
+
337
+ export const IMAGE_PROVIDER_CATALOG: ReadonlyArray<{ provider: ImageProvider; models: string[]; defaultModel: string }> = [
338
+ {
339
+ provider: 'openrouter',
340
+ models: Object.values(OPENROUTER_IMAGE_MODELS),
341
+ defaultModel: OPENROUTER_IMAGE_DEFAULTS.sheet,
342
+ },
343
+ ];
package/src/index.ts CHANGED
@@ -84,6 +84,21 @@ export {
84
84
  type EmbeddingUsage,
85
85
  } from './embedding-client.js';
86
86
 
87
+ export {
88
+ ImageClient,
89
+ resolveDefaultImageProvider,
90
+ OPENROUTER_IMAGE_MODELS,
91
+ OPENROUTER_IMAGE_DEFAULTS,
92
+ IMAGE_PROVIDER_CATALOG,
93
+ type ImageProvider,
94
+ type ImageClientOptions,
95
+ type ImageReference,
96
+ type ImageGenerateOptions,
97
+ type GeneratedImage,
98
+ type ImageGenerateResult,
99
+ type ImageModelCapabilities,
100
+ } from './image-client.js';
101
+
87
102
  export {
88
103
  parseJsonResponse,
89
104
  extractJsonFromText,