genai-lite 0.14.0 → 0.15.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.
Files changed (47) hide show
  1. package/README.md +22 -0
  2. package/dist/index.d.ts +4 -2
  3. package/dist/index.js +9 -1
  4. package/dist/llm/LLMService.d.ts +35 -3
  5. package/dist/llm/LLMService.js +673 -184
  6. package/dist/llm/clients/AnthropicClientAdapter.d.ts +11 -3
  7. package/dist/llm/clients/AnthropicClientAdapter.js +283 -56
  8. package/dist/llm/clients/GeminiClientAdapter.d.ts +9 -3
  9. package/dist/llm/clients/GeminiClientAdapter.js +215 -41
  10. package/dist/llm/clients/LlamaCppClientAdapter.d.ts +30 -7
  11. package/dist/llm/clients/LlamaCppClientAdapter.js +398 -90
  12. package/dist/llm/clients/LlamaCppServerClient.d.ts +31 -5
  13. package/dist/llm/clients/LlamaCppServerClient.js +50 -4
  14. package/dist/llm/clients/MistralClientAdapter.d.ts +10 -3
  15. package/dist/llm/clients/MistralClientAdapter.js +250 -47
  16. package/dist/llm/clients/MockClientAdapter.d.ts +6 -3
  17. package/dist/llm/clients/MockClientAdapter.js +135 -11
  18. package/dist/llm/clients/OpenAIClientAdapter.d.ts +9 -3
  19. package/dist/llm/clients/OpenAIClientAdapter.js +189 -38
  20. package/dist/llm/clients/OpenRouterClientAdapter.d.ts +9 -3
  21. package/dist/llm/clients/OpenRouterClientAdapter.js +190 -38
  22. package/dist/llm/clients/llamaCppState.d.ts +17 -0
  23. package/dist/llm/clients/llamaCppState.js +83 -0
  24. package/dist/llm/clients/preparedAdapterUtils.d.ts +31 -0
  25. package/dist/llm/clients/preparedAdapterUtils.js +195 -0
  26. package/dist/llm/clients/types.d.ts +79 -2
  27. package/dist/llm/clients/types.js +8 -0
  28. package/dist/llm/config.js +7 -1
  29. package/dist/llm/services/ModelResolver.d.ts +4 -2
  30. package/dist/llm/services/ModelResolver.js +29 -12
  31. package/dist/llm/services/RequestValidator.js +29 -0
  32. package/dist/llm/services/SettingsManager.js +22 -0
  33. package/dist/llm/tokenization/bounds.d.ts +11 -0
  34. package/dist/llm/tokenization/bounds.js +132 -0
  35. package/dist/llm/tokenization/index.d.ts +4 -0
  36. package/dist/llm/tokenization/index.js +13 -0
  37. package/dist/llm/tokenization/profiles.d.ts +8 -0
  38. package/dist/llm/tokenization/profiles.js +174 -0
  39. package/dist/llm/tokenization/types.d.ts +46 -0
  40. package/dist/llm/tokenization/types.js +4 -0
  41. package/dist/llm/types.d.ts +229 -3
  42. package/dist/prompting/content.js +11 -0
  43. package/dist/prompting/index.d.ts +6 -4
  44. package/dist/prompting/index.js +9 -1
  45. package/dist/shared/adapters/usageUtils.d.ts +21 -0
  46. package/dist/shared/adapters/usageUtils.js +103 -0
  47. package/package.json +5 -4
@@ -6,6 +6,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  };
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.LLMService = void 0;
9
+ const node_crypto_1 = require("node:crypto");
9
10
  const defaultLogger_1 = require("../logging/defaultLogger");
10
11
  const config_1 = require("./config");
11
12
  const template_1 = require("../prompting/template");
@@ -20,6 +21,8 @@ const SettingsManager_1 = require("./services/SettingsManager");
20
21
  const ModelResolver_1 = require("./services/ModelResolver");
21
22
  const withRetry_1 = require("../shared/services/withRetry");
22
23
  const types_1 = require("./clients/types");
24
+ const preparedAdapterUtils_1 = require("./clients/preparedAdapterUtils");
25
+ const tokenization_1 = require("./tokenization");
23
26
  /**
24
27
  * Main process service for LLM operations
25
28
  *
@@ -33,6 +36,7 @@ const types_1 = require("./clients/types");
33
36
  */
34
37
  class LLMService {
35
38
  constructor(getApiKey, options = {}) {
39
+ this.preparedCalls = new WeakMap();
36
40
  this.getApiKey = getApiKey;
37
41
  this.retryOptions = options.retry;
38
42
  this.defaultTimeoutMs = options.timeoutMs;
@@ -93,12 +97,14 @@ class LLMService {
93
97
  const resolvedModelId = resolved.modelId;
94
98
  const modelInfo = resolved.modelInfo;
95
99
  const source = this.getCapabilitySource(resolvedProviderId, resolvedModelId);
100
+ const capabilities = this.buildModelCapabilities(modelInfo, source);
96
101
  return {
97
102
  object: "model.capabilities",
98
103
  provider: resolvedProviderId,
99
104
  model: resolvedModelId,
100
105
  modelInfo: { ...modelInfo },
101
- structuredOutput: this.getStructuredOutputSupport(modelInfo, source),
106
+ structuredOutput: capabilities.structuredOutput,
107
+ capabilities,
102
108
  };
103
109
  }
104
110
  /**
@@ -171,81 +177,119 @@ class LLMService {
171
177
  */
172
178
  async sendMessage(request, callOptions) {
173
179
  this.logger.info(`LLMService.sendMessage called with presetId: ${request.presetId}, provider: ${request.providerId}, model: ${request.modelId}`);
180
+ const canonical = await this.prepareMessage(request, { mode: "complete" });
181
+ if (this.isFailureResponse(canonical)) {
182
+ return canonical;
183
+ }
184
+ return this.sendPrepared(canonical, callOptions);
185
+ }
186
+ /**
187
+ * Resolves and freezes the final semantic provider request without retrieving
188
+ * credentials or creating transport state.
189
+ */
190
+ async prepareMessage(request, options) {
191
+ if (!options ||
192
+ (options.mode !== "complete" && options.mode !== "stream")) {
193
+ return this.createPreparedFailure(request.providerId ?? "unknown", request.modelId ?? "unknown", types_1.ADAPTER_ERROR_CODES.INVALID_PREPARED_CALL, "Prepared-call mode must be either 'complete' or 'stream'.", "validation_error");
194
+ }
195
+ const result = await this.prepareCanonicalRequest(request, options.mode);
196
+ if ("error" in result) {
197
+ return result.error;
198
+ }
199
+ const handle = Object.create(null);
200
+ Object.defineProperties(handle, {
201
+ mode: {
202
+ value: options.mode,
203
+ enumerable: true,
204
+ configurable: false,
205
+ writable: false,
206
+ },
207
+ toJSON: {
208
+ value: () => {
209
+ throw new TypeError("Prepared LLM calls cannot be serialized.");
210
+ },
211
+ enumerable: false,
212
+ configurable: false,
213
+ writable: false,
214
+ },
215
+ });
216
+ Object.freeze(handle);
217
+ this.preparedCalls.set(handle, result.prepared);
218
+ return handle;
219
+ }
220
+ /** Returns the immutable inspection view for a service-owned prepared call. */
221
+ async inspectPrepared(handle) {
222
+ const prepared = this.preparedCalls.get(handle);
223
+ if (!prepared) {
224
+ return this.createPreparedHandleError(handle, types_1.ADAPTER_ERROR_CODES.INVALID_PREPARED_CALL, "The prepared call is invalid, forged, or belongs to another LLMService instance.");
225
+ }
226
+ return (0, preparedAdapterUtils_1.deepFreeze)({
227
+ provider: prepared.providerId,
228
+ model: prepared.modelId,
229
+ mode: prepared.adapterPrepared.mode,
230
+ request: structuredClone(prepared.adapterPrepared.requestView),
231
+ promptAccounting: structuredClone(prepared.adapterPrepared.promptAccounting),
232
+ ...(prepared.adapterPrepared.outputTokenLimit && {
233
+ outputTokenLimit: structuredClone(prepared.adapterPrepared.outputTokenLimit),
234
+ }),
235
+ bindings: structuredClone(prepared.adapterPrepared.bindings),
236
+ });
237
+ }
238
+ /** Dispatches a reusable complete-mode prepared call. */
239
+ async sendPrepared(handle, callOptions) {
240
+ const resolved = this.resolvePreparedHandle(handle, "complete");
241
+ if ("error" in resolved) {
242
+ return resolved.error;
243
+ }
244
+ const prepared = resolved.prepared;
245
+ const adapter = prepared.clientAdapter;
246
+ if (!adapter.sendPrepared) {
247
+ return this.createPreparedFailure(prepared.providerId, prepared.modelId, types_1.ADAPTER_ERROR_CODES.PREPARED_CALL_UNSUPPORTED, `Prepared calls are not supported for provider '${prepared.providerId}'.`, "unsupported_feature");
248
+ }
249
+ const apiKey = await this.resolveDispatchApiKey(prepared);
250
+ if (this.isFailureResponse(apiKey)) {
251
+ return apiKey;
252
+ }
253
+ const adapterOptions = this.createAdapterOptions(callOptions);
254
+ const retryOnTimeout = this.retryOptions?.retryOnTimeout ?? true;
255
+ const retryableCodes = new Set([
256
+ types_1.ADAPTER_ERROR_CODES.RATE_LIMIT_EXCEEDED,
257
+ types_1.ADAPTER_ERROR_CODES.NETWORK_ERROR,
258
+ ...(retryOnTimeout ? [types_1.ADAPTER_ERROR_CODES.REQUEST_TIMEOUT] : []),
259
+ ]);
174
260
  try {
175
- const preparedResult = await this.prepareRequest(request, callOptions);
176
- if ("error" in preparedResult) {
177
- return preparedResult.error;
178
- }
179
- const prepared = preparedResult.prepared;
180
- try {
181
- this.logger.info(`Making LLM request with ${prepared.clientAdapter.constructor.name} for provider: ${prepared.providerId}`);
182
- // Unified retry layer: adapters never throw, so retry decisions are made on
183
- // RETURNED failure responses. SDK-internal retries are disabled (maxRetries: 0
184
- // at client construction) — this loop is the single owner of retry behavior.
185
- const retryOnTimeout = this.retryOptions?.retryOnTimeout ?? true;
186
- const retryableCodes = new Set([
187
- types_1.ADAPTER_ERROR_CODES.RATE_LIMIT_EXCEEDED,
188
- types_1.ADAPTER_ERROR_CODES.NETWORK_ERROR,
189
- ...(retryOnTimeout ? [types_1.ADAPTER_ERROR_CODES.REQUEST_TIMEOUT] : []),
190
- ]);
191
- const result = await (0, withRetry_1.withRetry)(() => prepared.clientAdapter.sendMessage(prepared.internalRequest, prepared.apiKey, prepared.adapterOptions), (res) => {
192
- if (res.object !== 'error') {
193
- return { retry: false };
194
- }
195
- const code = String(res.error.code);
196
- const status = res.error.status;
197
- const retryable = retryableCodes.has(code) ||
198
- (code === types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR &&
199
- typeof status === 'number' &&
200
- (status === 408 || status === 409 || status >= 500));
201
- return { retry: retryable, retryAfterMs: res.error.retryAfterMs };
202
- }, {
203
- ...this.retryOptions,
204
- ...(callOptions?.maxRetries !== undefined && {
205
- maxRetries: callOptions.maxRetries,
206
- }),
207
- signal: callOptions?.signal,
208
- logger: this.logger,
209
- label: `${prepared.providerId}/${prepared.modelId}`,
210
- });
211
- const processedResult = this.postProcessResponse(result, prepared);
212
- if (processedResult.object === "chat.completion") {
213
- this.logger.info(`LLM request completed successfully for model: ${prepared.modelId}`);
261
+ const result = await (0, withRetry_1.withRetry)(async () => {
262
+ const revalidation = await this.revalidatePrepared(prepared, adapterOptions);
263
+ if (revalidation) {
264
+ return revalidation;
214
265
  }
215
- return processedResult;
216
- }
217
- catch (error) {
218
- this.logger.error("Error in LLMService.sendMessage:", error);
266
+ return adapter.sendPrepared(prepared.adapterPrepared, apiKey, adapterOptions);
267
+ }, (response) => {
268
+ if (response.object !== "error") {
269
+ return { retry: false };
270
+ }
271
+ const code = String(response.error.code);
272
+ const status = response.error.status;
219
273
  return {
220
- provider: prepared.providerId,
221
- model: prepared.modelId,
222
- error: {
223
- message: error instanceof Error
224
- ? error.message
225
- : "An unknown error occurred during message sending.",
226
- code: "PROVIDER_ERROR",
227
- type: "server_error",
228
- providerError: error,
229
- },
230
- object: "error",
274
+ retry: retryableCodes.has(code) ||
275
+ (code === types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR &&
276
+ typeof status === "number" &&
277
+ (status === 408 || status === 409 || status >= 500)),
278
+ retryAfterMs: response.error.retryAfterMs,
231
279
  };
232
- }
280
+ }, {
281
+ ...this.retryOptions,
282
+ ...(callOptions?.maxRetries !== undefined && {
283
+ maxRetries: callOptions.maxRetries,
284
+ }),
285
+ signal: callOptions?.signal,
286
+ logger: this.logger,
287
+ label: `${prepared.providerId}/${prepared.modelId}`,
288
+ });
289
+ return this.postProcessResponse(result, prepared);
233
290
  }
234
291
  catch (error) {
235
- this.logger.error("Error in LLMService.sendMessage (outer):", error);
236
- return {
237
- provider: request.providerId || request.presetId || 'unknown',
238
- model: request.modelId || request.presetId || 'unknown',
239
- error: {
240
- message: error instanceof Error
241
- ? error.message
242
- : "An unknown error occurred.",
243
- code: "UNEXPECTED_ERROR",
244
- type: "server_error",
245
- providerError: error,
246
- },
247
- object: "error",
248
- };
292
+ return this.createUnexpectedDispatchFailure(prepared, error);
249
293
  }
250
294
  }
251
295
  /**
@@ -256,83 +300,146 @@ class LLMService {
256
300
  * are yielded as a single `error` event.
257
301
  */
258
302
  async *streamMessage(request, callOptions) {
303
+ const attemptId = (0, node_crypto_1.randomUUID)();
259
304
  this.logger.info(`LLMService.streamMessage called with presetId: ${request.presetId}, provider: ${request.providerId}, model: ${request.modelId}`);
305
+ const canonical = await this.prepareMessage(request, { mode: "stream" });
306
+ if (this.isFailureResponse(canonical)) {
307
+ yield { attemptId, type: "error", error: canonical };
308
+ return;
309
+ }
310
+ yield* this.streamPreparedWithAttempt(canonical, callOptions, attemptId);
311
+ return;
312
+ }
313
+ /** Dispatches a reusable stream-mode prepared call with no automatic retries. */
314
+ async *streamPrepared(handle, callOptions) {
315
+ yield* this.streamPreparedWithAttempt(handle, callOptions, (0, node_crypto_1.randomUUID)());
316
+ }
317
+ async *streamPreparedWithAttempt(handle, callOptions, attemptId) {
318
+ const resolved = this.resolvePreparedHandle(handle, "stream");
319
+ if ("error" in resolved) {
320
+ yield { attemptId, type: "error", error: resolved.error };
321
+ return;
322
+ }
323
+ const prepared = resolved.prepared;
324
+ const adapter = prepared.clientAdapter;
325
+ if (!adapter.streamPrepared) {
326
+ yield {
327
+ attemptId,
328
+ type: "error",
329
+ error: this.createPreparedFailure(prepared.providerId, prepared.modelId, types_1.ADAPTER_ERROR_CODES.PREPARED_CALL_UNSUPPORTED, `Prepared streaming is not supported for provider '${prepared.providerId}'.`, "unsupported_feature"),
330
+ };
331
+ return;
332
+ }
333
+ const apiKey = await this.resolveDispatchApiKey(prepared);
334
+ if (this.isFailureResponse(apiKey)) {
335
+ yield { attemptId, type: "error", error: apiKey };
336
+ return;
337
+ }
338
+ const cancellation = new AbortController();
339
+ const signal = callOptions?.signal
340
+ ? AbortSignal.any([callOptions.signal, cancellation.signal])
341
+ : cancellation.signal;
342
+ const adapterOptions = this.createAdapterOptions({
343
+ ...callOptions,
344
+ signal,
345
+ });
346
+ const partialState = {
347
+ provider: prepared.providerId,
348
+ model: prepared.modelId,
349
+ started: false,
350
+ choices: new Map(),
351
+ };
352
+ let iterator;
353
+ let terminal = false;
260
354
  try {
261
- const preparedResult = await this.prepareRequest(request, callOptions);
262
- if ("error" in preparedResult) {
263
- yield { type: "error", error: preparedResult.error };
264
- return;
265
- }
266
- const prepared = preparedResult.prepared;
267
- if (!prepared.clientAdapter.streamMessage) {
268
- yield {
269
- type: "error",
270
- error: {
271
- provider: prepared.providerId,
272
- model: prepared.modelId,
273
- error: {
274
- message: `Streaming is not supported for provider '${prepared.providerId}'.`,
275
- code: "PROVIDER_ERROR",
276
- type: "unsupported_feature",
277
- },
278
- object: "error",
279
- },
280
- };
355
+ const revalidation = await this.revalidatePrepared(prepared, adapterOptions);
356
+ if (revalidation) {
357
+ yield { attemptId, type: "error", error: revalidation };
281
358
  return;
282
359
  }
283
- try {
284
- for await (const event of prepared.clientAdapter.streamMessage(prepared.internalRequest, prepared.apiKey, prepared.adapterOptions)) {
285
- if (event.type === "complete") {
286
- const processedResult = this.postProcessResponse(event.response, prepared);
287
- if (processedResult.object === "error") {
288
- yield { type: "error", error: processedResult };
289
- }
290
- else {
291
- yield { type: "complete", response: processedResult };
292
- }
293
- continue;
360
+ iterator = adapter
361
+ .streamPrepared(prepared.adapterPrepared, apiKey, adapterOptions)[Symbol.asyncIterator]();
362
+ while (!terminal) {
363
+ const step = await this.nextStreamEvent(iterator, signal);
364
+ if (step.done) {
365
+ break;
366
+ }
367
+ const event = step.value;
368
+ this.observeStreamPartial(partialState, event);
369
+ if (event.type === "adapter_evidence") {
370
+ continue;
371
+ }
372
+ if (event.type === "complete") {
373
+ const processed = this.postProcessResponse(event.response, prepared);
374
+ if (processed.object === "error") {
375
+ const failure = this.finalizeStreamFailure(processed, partialState, attemptId, prepared);
376
+ terminal = true;
377
+ yield {
378
+ attemptId,
379
+ type: "error",
380
+ error: failure,
381
+ };
382
+ }
383
+ else {
384
+ terminal = true;
385
+ yield { attemptId, type: "complete", response: processed };
294
386
  }
295
- yield event;
387
+ break;
296
388
  }
389
+ if (event.type === "error") {
390
+ const failure = this.finalizeStreamFailure(event.error, partialState, attemptId, prepared);
391
+ terminal = true;
392
+ yield {
393
+ attemptId,
394
+ type: "error",
395
+ error: failure,
396
+ };
397
+ break;
398
+ }
399
+ const { observedEvidence: _observedEvidence, ...publicEvent } = event;
400
+ yield { ...publicEvent, attemptId };
297
401
  }
298
- catch (error) {
299
- this.logger.error("Error in LLMService.streamMessage:", error);
402
+ if (!terminal) {
403
+ terminal = true;
404
+ const aborted = signal.aborted;
300
405
  yield {
406
+ attemptId,
301
407
  type: "error",
302
- error: {
303
- provider: prepared.providerId,
304
- model: prepared.modelId,
305
- error: {
306
- message: error instanceof Error
307
- ? error.message
308
- : "An unknown error occurred during message streaming.",
309
- code: "PROVIDER_ERROR",
310
- type: "server_error",
311
- providerError: error,
312
- },
313
- object: "error",
314
- },
408
+ error: this.finalizeStreamFailure(this.createPreparedFailure(prepared.providerId, prepared.modelId, aborted
409
+ ? types_1.ADAPTER_ERROR_CODES.REQUEST_ABORTED
410
+ : types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR, aborted
411
+ ? "The streaming request was aborted."
412
+ : "The provider stream ended without a terminal event.", aborted ? "abort_error" : "server_error"), partialState, attemptId, prepared),
315
413
  };
316
414
  }
317
415
  }
318
416
  catch (error) {
319
- this.logger.error("Error in LLMService.streamMessage (outer):", error);
320
- yield {
321
- type: "error",
322
- error: {
323
- provider: request.providerId || request.presetId || 'unknown',
324
- model: request.modelId || request.presetId || 'unknown',
325
- error: {
326
- message: error instanceof Error
417
+ if (!terminal) {
418
+ terminal = true;
419
+ const aborted = signal.aborted;
420
+ yield {
421
+ attemptId,
422
+ type: "error",
423
+ error: this.finalizeStreamFailure(this.createPreparedFailure(prepared.providerId, prepared.modelId, aborted
424
+ ? types_1.ADAPTER_ERROR_CODES.REQUEST_ABORTED
425
+ : types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR, aborted
426
+ ? "The streaming request was aborted."
427
+ : error instanceof Error
327
428
  ? error.message
328
- : "An unknown error occurred.",
329
- code: "UNEXPECTED_ERROR",
330
- type: "server_error",
331
- providerError: error,
332
- },
333
- object: "error",
334
- },
335
- };
429
+ : "An unknown streaming error occurred.", aborted ? "abort_error" : "server_error", error), partialState, attemptId, prepared),
430
+ };
431
+ }
432
+ }
433
+ finally {
434
+ cancellation.abort();
435
+ if (iterator?.return) {
436
+ try {
437
+ void Promise.resolve(iterator.return()).catch(() => undefined);
438
+ }
439
+ catch {
440
+ // Iterator cleanup is best-effort and must never replace the terminal.
441
+ }
442
+ }
336
443
  }
337
444
  }
338
445
  async resolveAndValidateCapabilities(request, options) {
@@ -386,12 +493,45 @@ class LLMService {
386
493
  resolvedRequest,
387
494
  finalSettings,
388
495
  capabilities,
496
+ ...(resolved.adapterPreparationState !== undefined && {
497
+ adapterPreparationState: resolved.adapterPreparationState,
498
+ }),
389
499
  },
390
500
  };
391
501
  }
392
502
  buildModelCapabilities(modelInfo, source) {
503
+ const structuredOutput = this.getStructuredOutputSupport(modelInfo, source);
504
+ const tokenProfile = (0, tokenization_1.resolveTokenProfile)(modelInfo.providerId, modelInfo.id);
505
+ const reportsUsage = modelInfo.providerId === "openai" ||
506
+ modelInfo.providerId === "anthropic" ||
507
+ modelInfo.providerId === "gemini" ||
508
+ modelInfo.providerId === "mistral" ||
509
+ modelInfo.providerId === "openrouter" ||
510
+ modelInfo.providerId === "llamacpp";
393
511
  return {
394
- structuredOutput: this.getStructuredOutputSupport(modelInfo, source),
512
+ structuredOutput,
513
+ ...(modelInfo.contextWindow !== undefined && {
514
+ contextWindow: {
515
+ tokens: modelInfo.contextWindow,
516
+ source,
517
+ },
518
+ }),
519
+ contentTokenCounting: tokenProfile.status === "available" ? "exact" : "unavailable",
520
+ preparedMessageTokenCounting: modelInfo.providerId === "llamacpp" ? "runtime" : "unavailable",
521
+ ...(tokenProfile.status === "available" && {
522
+ tokenProfileId: tokenProfile.profile.id,
523
+ tokenProfileMappingRevision: tokenProfile.mappingRevision,
524
+ }),
525
+ ...(reportsUsage && {
526
+ reportsPromptUsage: true,
527
+ reportsCompletionUsage: true,
528
+ }),
529
+ distinguishesLimitCause: modelInfo.providerId === "anthropic" ||
530
+ modelInfo.providerId === "gemini",
531
+ structuredOutputDelivery: {
532
+ native: structuredOutput,
533
+ prompt: "instruction_only",
534
+ },
395
535
  };
396
536
  }
397
537
  getStructuredOutputSupport(modelInfo, source) {
@@ -415,7 +555,329 @@ class LLMService {
415
555
  getCapabilitySource(providerId, modelId) {
416
556
  return (0, config_1.getModelById)(modelId, providerId) ? "registry" : "fallback";
417
557
  }
418
- async prepareRequest(request, callOptions) {
558
+ isFailureResponse(value) {
559
+ return (typeof value === "object" &&
560
+ value !== null &&
561
+ value.object === "error");
562
+ }
563
+ createPreparedFailure(provider, model, code, message, type, providerError) {
564
+ return {
565
+ provider,
566
+ model,
567
+ error: {
568
+ message,
569
+ code,
570
+ type,
571
+ ...(providerError !== undefined && { providerError }),
572
+ },
573
+ object: "error",
574
+ };
575
+ }
576
+ createPreparedHandleError(handle, code, message) {
577
+ const mode = typeof handle === "object" && handle !== null && "mode" in handle
578
+ ? String(handle.mode ?? "unknown")
579
+ : "unknown";
580
+ return this.createPreparedFailure("unknown", "unknown", code, message, mode === "unknown" ? "invalid_request_error" : "validation_error");
581
+ }
582
+ resolvePreparedHandle(handle, expectedMode) {
583
+ if ((typeof handle !== "object" && typeof handle !== "function") ||
584
+ handle === null) {
585
+ return {
586
+ error: this.createPreparedHandleError(handle, types_1.ADAPTER_ERROR_CODES.INVALID_PREPARED_CALL, "The prepared call is invalid or forged."),
587
+ };
588
+ }
589
+ const prepared = this.preparedCalls.get(handle);
590
+ if (!prepared) {
591
+ return {
592
+ error: this.createPreparedHandleError(handle, types_1.ADAPTER_ERROR_CODES.INVALID_PREPARED_CALL, "The prepared call is invalid, forged, or belongs to another LLMService instance."),
593
+ };
594
+ }
595
+ if (prepared.adapterPrepared.mode !== expectedMode) {
596
+ return {
597
+ error: this.createPreparedFailure(prepared.providerId, prepared.modelId, types_1.ADAPTER_ERROR_CODES.PREPARED_CALL_MODE_MISMATCH, `A '${prepared.adapterPrepared.mode}' prepared call cannot be dispatched as '${expectedMode}'.`, "validation_error"),
598
+ };
599
+ }
600
+ return { prepared };
601
+ }
602
+ async resolveDispatchApiKey(prepared) {
603
+ try {
604
+ const apiKey = await this.getApiKey(prepared.providerId);
605
+ if (!apiKey) {
606
+ return this.createPreparedFailure(prepared.providerId, prepared.modelId, "API_KEY_ERROR", `API key for provider '${prepared.providerId}' could not be retrieved. Ensure your ApiKeyProvider is configured correctly.`, "authentication_error");
607
+ }
608
+ if (prepared.clientAdapter.validateApiKey &&
609
+ !prepared.clientAdapter.validateApiKey(apiKey)) {
610
+ return this.createPreparedFailure(prepared.providerId, prepared.modelId, types_1.ADAPTER_ERROR_CODES.INVALID_API_KEY, `Invalid API key format for provider '${prepared.providerId}'. Please check your API key.`, "authentication_error");
611
+ }
612
+ return apiKey;
613
+ }
614
+ catch (error) {
615
+ return this.createPreparedFailure(prepared.providerId, prepared.modelId, types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR, error instanceof Error
616
+ ? error.message
617
+ : "An unknown error occurred while retrieving credentials.", "server_error", error);
618
+ }
619
+ }
620
+ createAdapterOptions(callOptions) {
621
+ const timeoutMs = callOptions?.timeoutMs ?? this.defaultTimeoutMs;
622
+ return {
623
+ ...(callOptions?.signal && { signal: callOptions.signal }),
624
+ ...(timeoutMs !== undefined && { timeoutMs }),
625
+ };
626
+ }
627
+ async revalidatePrepared(prepared, options) {
628
+ if (!prepared.clientAdapter.revalidatePreparedRequest) {
629
+ return undefined;
630
+ }
631
+ const result = await prepared.clientAdapter.revalidatePreparedRequest(prepared.adapterPrepared, options);
632
+ return result.valid ? undefined : result.error;
633
+ }
634
+ async nextStreamEvent(iterator, signal) {
635
+ if (signal.aborted) {
636
+ throw new Error("Streaming request aborted");
637
+ }
638
+ return new Promise((resolve, reject) => {
639
+ const onAbort = () => {
640
+ reject(new Error("Streaming request aborted"));
641
+ };
642
+ signal.addEventListener("abort", onAbort, { once: true });
643
+ Promise.resolve(iterator.next()).then((result) => {
644
+ signal.removeEventListener("abort", onAbort);
645
+ resolve(result);
646
+ }, (error) => {
647
+ signal.removeEventListener("abort", onAbort);
648
+ reject(error);
649
+ });
650
+ });
651
+ }
652
+ observeStreamPartial(state, event) {
653
+ const observed = event.observedEvidence;
654
+ if (observed?.choice) {
655
+ const choice = this.getStreamPartialChoice(state, observed.choice.index);
656
+ if (observed.choice.rawContentDelta !== undefined) {
657
+ choice.rawContent =
658
+ (choice.rawContent ?? "") + observed.choice.rawContentDelta;
659
+ }
660
+ if (observed.choice.rawContentParts) {
661
+ choice.rawContentParts = [
662
+ ...(choice.rawContentParts ?? []),
663
+ ...observed.choice.rawContentParts,
664
+ ];
665
+ }
666
+ if (observed.choice.rawAnswerAccounting) {
667
+ choice.rawAnswerAccounting =
668
+ observed.choice.rawAnswerAccounting;
669
+ }
670
+ if (observed.choice.finishReason !== undefined) {
671
+ choice.finishReason = observed.choice.finishReason;
672
+ }
673
+ if (observed.choice.termination) {
674
+ choice.termination = observed.choice.termination;
675
+ }
676
+ }
677
+ if (observed?.usageEvidence) {
678
+ state.usageEvidence = {
679
+ ...(state.usageEvidence ?? {}),
680
+ ...observed.usageEvidence,
681
+ };
682
+ }
683
+ if (observed?.usage) {
684
+ state.usage = {
685
+ ...(state.usage ?? {}),
686
+ ...Object.fromEntries(Object.entries(observed.usage).filter(([, value]) => value !== undefined)),
687
+ };
688
+ }
689
+ if (event.type === "adapter_evidence") {
690
+ return;
691
+ }
692
+ if (event.type === "start") {
693
+ state.started = true;
694
+ state.provider = event.provider;
695
+ state.model = event.model;
696
+ state.id = event.id;
697
+ state.created = event.created;
698
+ return;
699
+ }
700
+ if (event.type === "content_delta" || event.type === "reasoning_delta") {
701
+ const choice = this.getStreamPartialChoice(state, event.index);
702
+ if (event.type === "content_delta") {
703
+ choice.content += event.delta;
704
+ }
705
+ else {
706
+ choice.reasoning += event.delta;
707
+ }
708
+ return;
709
+ }
710
+ if (event.type === "usage") {
711
+ state.usage = {
712
+ ...(state.usage ?? {}),
713
+ ...Object.fromEntries(Object.entries(event.usage).filter(([, value]) => value !== undefined)),
714
+ };
715
+ }
716
+ }
717
+ getStreamPartialChoice(state, index) {
718
+ let choice = state.choices.get(index);
719
+ if (!choice) {
720
+ choice = { content: "", reasoning: "" };
721
+ state.choices.set(index, choice);
722
+ }
723
+ return choice;
724
+ }
725
+ finalizeStreamFailure(failure, state, attemptId, prepared) {
726
+ const processed = this.postProcessResponse(this.attachStreamPartial(failure, state, attemptId), prepared);
727
+ return processed;
728
+ }
729
+ attachStreamPartial(failure, state, attemptId) {
730
+ const hasEvidence = state.started ||
731
+ state.choices.size > 0 ||
732
+ state.usage !== undefined ||
733
+ state.usageEvidence !== undefined;
734
+ if (!hasEvidence) {
735
+ return failure;
736
+ }
737
+ const choices = Array.from(state.choices.entries())
738
+ .sort(([left], [right]) => left - right)
739
+ .map(([index, choice]) => ({
740
+ index,
741
+ message: {
742
+ role: "assistant",
743
+ content: choice.content,
744
+ },
745
+ ...(choice.reasoning.length > 0 && {
746
+ reasoning: choice.reasoning,
747
+ }),
748
+ ...(choice.rawContent !== undefined && {
749
+ rawContent: choice.rawContent,
750
+ }),
751
+ ...(choice.rawContentParts && {
752
+ rawContentParts: choice.rawContentParts,
753
+ }),
754
+ ...(choice.rawAnswerAccounting && {
755
+ rawAnswerAccounting: choice.rawAnswerAccounting,
756
+ }),
757
+ finish_reason: choice.finishReason ?? null,
758
+ termination: choice.termination ?? {
759
+ rawReason: null,
760
+ kind: "unknown",
761
+ },
762
+ }));
763
+ const observed = {
764
+ id: state.id ?? attemptId,
765
+ provider: state.provider,
766
+ model: state.model,
767
+ created: state.created ?? Math.floor(Date.now() / 1000),
768
+ choices,
769
+ ...(state.usage && { usage: state.usage }),
770
+ ...(state.usageEvidence && {
771
+ usageEvidence: state.usageEvidence,
772
+ }),
773
+ };
774
+ if (!failure.partialResponse) {
775
+ return { ...failure, partialResponse: observed };
776
+ }
777
+ const provided = failure.partialResponse;
778
+ const choicesByIndex = new Map(observed.choices.map((choice, position) => [
779
+ choice.index ?? position,
780
+ choice,
781
+ ]));
782
+ for (const [position, choice] of provided.choices.entries()) {
783
+ const index = choice.index ?? position;
784
+ const prior = choicesByIndex.get(index);
785
+ choicesByIndex.set(index, {
786
+ ...(prior ?? {}),
787
+ ...choice,
788
+ message: {
789
+ ...(prior?.message ?? { role: "assistant", content: "" }),
790
+ ...choice.message,
791
+ },
792
+ ...(choice.rawContentParts === undefined &&
793
+ prior?.rawContentParts && {
794
+ rawContentParts: prior.rawContentParts,
795
+ }),
796
+ });
797
+ }
798
+ const usage = {
799
+ ...(observed.usage ?? {}),
800
+ ...Object.fromEntries(Object.entries(provided.usage ?? {}).filter(([, value]) => value !== undefined)),
801
+ };
802
+ const usageEvidence = {
803
+ ...(observed.usageEvidence ?? {}),
804
+ ...Object.fromEntries(Object.entries(provided.usageEvidence ?? {}).filter(([, value]) => value !== undefined)),
805
+ };
806
+ return {
807
+ ...failure,
808
+ partialResponse: {
809
+ ...observed,
810
+ ...provided,
811
+ choices: Array.from(choicesByIndex.entries())
812
+ .sort(([left], [right]) => left - right)
813
+ .map(([, choice]) => choice),
814
+ ...(Object.keys(usage).length > 0 && { usage }),
815
+ ...(Object.keys(usageEvidence).length > 0 && {
816
+ usageEvidence,
817
+ }),
818
+ },
819
+ };
820
+ }
821
+ isValidPreparedAccounting(accounting) {
822
+ if (accounting.status === "unavailable") {
823
+ return true;
824
+ }
825
+ return accounting.count !== undefined || accounting.upperBound !== undefined;
826
+ }
827
+ createEffectiveOutputTokenLimit(request, providerId, modelInfo, filteredMaxTokens) {
828
+ if (filteredMaxTokens === undefined ||
829
+ !Number.isSafeInteger(filteredMaxTokens) ||
830
+ filteredMaxTokens <= 0) {
831
+ return undefined;
832
+ }
833
+ const presetId = request.presetId;
834
+ const preset = presetId
835
+ ? this.presetManager
836
+ .getPresets()
837
+ .find((candidate) => candidate.id === presetId)
838
+ : undefined;
839
+ const source = request.settings?.maxTokens !== undefined
840
+ ? "request"
841
+ : preset?.settings.maxTokens !== undefined
842
+ ? "preset"
843
+ : modelInfo.defaultSettings?.maxTokens !== undefined ||
844
+ modelInfo.maxTokens !== undefined
845
+ ? "model_default"
846
+ : "library_default";
847
+ const hardLimit = modelInfo.hardOutputTokenLimit;
848
+ const tokens = hardLimit
849
+ ? Math.min(filteredMaxTokens, hardLimit.tokens)
850
+ : filteredMaxTokens;
851
+ const counts = hardLimit?.counts ??
852
+ (providerId === "mistral"
853
+ ? "visible_output"
854
+ : providerId === "openrouter"
855
+ ? "provider_defined"
856
+ : providerId === "openai" ||
857
+ providerId === "anthropic" ||
858
+ providerId === "gemini" ||
859
+ providerId === "llamacpp"
860
+ ? "visible_and_reasoning"
861
+ : "unknown");
862
+ return {
863
+ tokens,
864
+ source,
865
+ ...(tokens !== filteredMaxTokens && {
866
+ requestedTokens: filteredMaxTokens,
867
+ clamp: {
868
+ tokens,
869
+ source: hardLimit.source,
870
+ },
871
+ }),
872
+ counts,
873
+ };
874
+ }
875
+ createUnexpectedDispatchFailure(prepared, error) {
876
+ return this.createPreparedFailure(prepared.providerId, prepared.modelId, types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR, error instanceof Error
877
+ ? error.message
878
+ : "An unknown error occurred during prepared dispatch.", "server_error", error);
879
+ }
880
+ async prepareCanonicalRequest(request, mode) {
419
881
  const validation = await this.resolveAndValidateCapabilities(request, {
420
882
  validateStructure: true,
421
883
  detectLocalCapabilities: true,
@@ -423,7 +885,7 @@ class LLMService {
423
885
  if ("error" in validation) {
424
886
  return { error: validation.error };
425
887
  }
426
- const { providerId, modelId, modelInfo, resolvedRequest, finalSettings, } = validation.context;
888
+ const { providerId, modelId, modelInfo, resolvedRequest, finalSettings, adapterPreparationState, } = validation.context;
427
889
  // Get provider info for parameter filtering
428
890
  const providerInfo = (0, config_1.getProviderById)(providerId);
429
891
  if (!providerInfo) {
@@ -442,6 +904,11 @@ class LLMService {
442
904
  }
443
905
  // Filter out unsupported parameters
444
906
  const filteredSettings = this.settingsManager.filterUnsupportedParameters(finalSettings, modelInfo, providerInfo);
907
+ const outputTokenLimit = this.createEffectiveOutputTokenLimit(request, providerId, modelInfo, filteredSettings.maxTokens);
908
+ if (outputTokenLimit?.clamp &&
909
+ filteredSettings.maxTokens !== outputTokenLimit.tokens) {
910
+ filteredSettings.maxTokens = outputTokenLimit.tokens;
911
+ }
445
912
  const internalRequest = {
446
913
  ...resolvedRequest,
447
914
  settings: filteredSettings,
@@ -454,43 +921,34 @@ class LLMService {
454
921
  });
455
922
  // Get client adapter
456
923
  const clientAdapter = this.adapterRegistry.getAdapter(providerId);
924
+ if (!clientAdapter.prepareRequest) {
925
+ return {
926
+ error: this.createPreparedFailure(providerId, modelId, types_1.ADAPTER_ERROR_CODES.PREPARED_CALL_UNSUPPORTED, `Prepared calls are not supported for provider '${providerId}'.`, "unsupported_feature"),
927
+ };
928
+ }
457
929
  try {
458
- const apiKey = await this.getApiKey(providerId);
459
- if (!apiKey) {
930
+ const adapterResult = await clientAdapter.prepareRequest(internalRequest, {
931
+ mode,
932
+ modelInfo,
933
+ outputTokenLimit,
934
+ ...(adapterPreparationState !== undefined && {
935
+ providerState: adapterPreparationState,
936
+ }),
937
+ });
938
+ if ("error" in adapterResult) {
939
+ return { error: adapterResult.error };
940
+ }
941
+ if (!this.isValidPreparedAccounting(adapterResult.prepared.promptAccounting)) {
460
942
  return {
461
- error: {
462
- provider: providerId,
463
- model: modelId,
464
- error: {
465
- message: `API key for provider '${providerId}' could not be retrieved. Ensure your ApiKeyProvider is configured correctly.`,
466
- code: "API_KEY_ERROR",
467
- type: "authentication_error",
468
- },
469
- object: "error",
470
- },
943
+ error: this.createPreparedFailure(providerId, modelId, types_1.ADAPTER_ERROR_CODES.INVALID_PREPARED_CALL, "The adapter returned invalid prepared prompt accounting.", "server_error"),
471
944
  };
472
945
  }
473
- // Validate API key format if adapter supports it
474
- if (clientAdapter.validateApiKey && !clientAdapter.validateApiKey(apiKey)) {
946
+ if (adapterResult.prepared.mode !== mode) {
475
947
  return {
476
- error: {
477
- provider: providerId,
478
- model: modelId,
479
- error: {
480
- message: `Invalid API key format for provider '${providerId}'. Please check your API key.`,
481
- code: "INVALID_API_KEY",
482
- type: "authentication_error",
483
- },
484
- object: "error",
485
- },
948
+ error: this.createPreparedFailure(providerId, modelId, types_1.ADAPTER_ERROR_CODES.INVALID_PREPARED_CALL, `The adapter prepared mode '${String(adapterResult.prepared.mode)}' instead of '${mode}'.`, "server_error"),
486
949
  };
487
950
  }
488
- const adapterOptions = {
489
- ...(callOptions?.signal && { signal: callOptions.signal }),
490
- ...((callOptions?.timeoutMs ?? this.defaultTimeoutMs) !== undefined && {
491
- timeoutMs: callOptions?.timeoutMs ?? this.defaultTimeoutMs,
492
- }),
493
- };
951
+ const adapterPrepared = (0, preparedAdapterUtils_1.deepFreeze)(adapterResult.prepared);
494
952
  return {
495
953
  prepared: {
496
954
  providerId,
@@ -499,33 +957,59 @@ class LLMService {
499
957
  resolvedRequest,
500
958
  internalRequest,
501
959
  clientAdapter,
502
- apiKey,
503
- adapterOptions,
960
+ adapterPrepared,
504
961
  },
505
962
  };
506
963
  }
507
964
  catch (error) {
508
- this.logger.error("Error preparing LLM request:", error);
509
965
  return {
510
- error: {
511
- provider: providerId,
512
- model: modelId,
513
- error: {
514
- message: error instanceof Error
515
- ? error.message
516
- : "An unknown error occurred during request preparation.",
517
- code: "PROVIDER_ERROR",
518
- type: "server_error",
519
- providerError: error,
520
- },
521
- object: "error",
522
- },
966
+ error: this.createPreparedFailure(providerId, modelId, types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR, error instanceof Error
967
+ ? error.message
968
+ : "An unknown error occurred during request preparation.", "server_error", error),
523
969
  };
524
970
  }
525
971
  }
526
972
  postProcessResponse(result, prepared) {
527
973
  if (result.object === "error") {
528
- return result;
974
+ if (!result.partialResponse) {
975
+ return result;
976
+ }
977
+ const processed = this.postProcessResponse({
978
+ ...result.partialResponse,
979
+ object: "chat.completion",
980
+ }, prepared);
981
+ if (processed.object === "error") {
982
+ return {
983
+ ...result,
984
+ ...(processed.partialResponse && {
985
+ partialResponse: processed.partialResponse,
986
+ }),
987
+ };
988
+ }
989
+ const { object: _object, ...partialResponse } = processed;
990
+ return { ...result, partialResponse };
991
+ }
992
+ const tokenProfile = (0, tokenization_1.resolveTokenProfile)(prepared.providerId, prepared.modelId);
993
+ if (tokenProfile.status === "available") {
994
+ for (const choice of result.choices) {
995
+ const rawContent = choice.rawContent ?? choice.message.content;
996
+ choice.rawContent ?? (choice.rawContent = rawContent);
997
+ if (!choice.rawAnswerAccounting) {
998
+ const counted = (0, tokenization_1.countTextTokens)(rawContent, tokenProfile.profile);
999
+ if (counted.status === "available") {
1000
+ choice.rawAnswerAccounting = {
1001
+ tokens: counted.count.tokens,
1002
+ method: counted.count.method,
1003
+ source: "library",
1004
+ tokenizerId: counted.count.tokenizerId,
1005
+ tokenProfileRevision: counted.count.tokenProfileRevision,
1006
+ reasoning: choice.reasoning && choice.reasoning.length > 0
1007
+ ? "excluded"
1008
+ : "unknown",
1009
+ };
1010
+ }
1011
+ }
1012
+ }
529
1013
  }
530
1014
  // Post-process for thinking tag fallback
531
1015
  // This feature extracts reasoning from XML tags when native reasoning is not active.
@@ -542,6 +1026,7 @@ class LLMService {
542
1026
  // Process the response - extract thinking tags if present
543
1027
  const choice = result.choices[0];
544
1028
  if (choice?.message?.content) {
1029
+ choice.rawContent ?? (choice.rawContent = choice.message.content);
545
1030
  const { extracted, remaining } = (0, parser_1.extractInitialTaggedContent)(choice.message.content, tagName);
546
1031
  if (extracted !== null) {
547
1032
  // Success: thinking tag found
@@ -557,6 +1042,9 @@ class LLMService {
557
1042
  choice.reasoning = extracted;
558
1043
  }
559
1044
  choice.message.content = remaining;
1045
+ if (choice.rawAnswerAccounting) {
1046
+ choice.rawAnswerAccounting.reasoning = "included_extracted";
1047
+ }
560
1048
  }
561
1049
  else {
562
1050
  // Tag was not found
@@ -585,7 +1073,8 @@ class LLMService {
585
1073
  model: result.model,
586
1074
  created: result.created,
587
1075
  choices: result.choices,
588
- usage: result.usage
1076
+ usage: result.usage,
1077
+ usageEvidence: result.usageEvidence,
589
1078
  }
590
1079
  };
591
1080
  }