call-ai 0.8.3 → 0.8.5-dev-preview

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/api.js CHANGED
@@ -1,7 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getMeta = void 0;
3
4
  exports.callAI = callAI;
4
5
  const strategies_1 = require("./strategies");
6
+ const response_metadata_1 = require("./response-metadata");
7
+ Object.defineProperty(exports, "getMeta", { enumerable: true, get: function () { return response_metadata_1.getMeta; } });
8
+ const key_management_1 = require("./key-management");
9
+ const error_handling_1 = require("./error-handling");
10
+ const api_core_1 = require("./api-core");
11
+ const non_streaming_1 = require("./non-streaming");
12
+ const streaming_1 = require("./streaming");
5
13
  // Import package version for debugging
6
14
  // eslint-disable-next-line @typescript-eslint/no-var-requires
7
15
  const PACKAGE_VERSION = require("../package.json").version;
@@ -18,6 +26,9 @@ const FALLBACK_MODEL = "openrouter/auto";
18
26
  function callAI(prompt, options = {}) {
19
27
  // Check if we need to force streaming based on model strategy
20
28
  const schemaStrategy = (0, strategies_1.chooseSchemaStrategy)(options.model, options.schema || null);
29
+ if (!options.max_tokens) {
30
+ options.max_tokens = 100000;
31
+ }
21
32
  // Handle special case: Claude with tools requires streaming
22
33
  if (!options.stream && schemaStrategy.shouldForceStream) {
23
34
  // Buffer streaming results into a single response
@@ -32,7 +43,9 @@ function callAI(prompt, options = {}) {
32
43
  const streamPromise = (async () => {
33
44
  // Do setup and validation before returning the generator
34
45
  const { endpoint, requestOptions, model, schemaStrategy } = prepareRequestParams(prompt, { ...options, stream: true });
35
- if (options.debug) {
46
+ // Use either explicit debug option or global debug flag
47
+ const debug = options.debug || key_management_1.globalDebug;
48
+ if (debug) {
36
49
  console.log(`[callAI:${PACKAGE_VERSION}] Making fetch request to: ${endpoint}`);
37
50
  console.log(`[callAI:${PACKAGE_VERSION}] With model: ${model}`);
38
51
  console.log(`[callAI:${PACKAGE_VERSION}] Request headers:`, JSON.stringify(requestOptions.headers));
@@ -90,7 +103,7 @@ function callAI(prompt, options = {}) {
90
103
  let isInvalidModel = false;
91
104
  try {
92
105
  // Check if this is an invalid model error
93
- const modelCheckResult = await checkForInvalidModelError(clonedResponse, model, false, options.skipRetry, options.debug);
106
+ const modelCheckResult = await (0, error_handling_1.checkForInvalidModelError)(clonedResponse, model, options.debug);
94
107
  isInvalidModel = modelCheckResult.isInvalidModel;
95
108
  if (isInvalidModel) {
96
109
  if (options.debug) {
@@ -208,7 +221,7 @@ function callAI(prompt, options = {}) {
208
221
  if (options.debug) {
209
222
  console.log(`[callAI:${PACKAGE_VERSION}] Response OK, creating streaming generator`);
210
223
  }
211
- return createStreamingGenerator(response, options, schemaStrategy, model);
224
+ return (0, streaming_1.createStreamingGenerator)(response, options, schemaStrategy, model);
212
225
  })();
213
226
  // For backward compatibility with v0.6.x where users didn't await the result
214
227
  if (process.env.NODE_ENV !== "production") {
@@ -218,7 +231,7 @@ function callAI(prompt, options = {}) {
218
231
  }
219
232
  // Create a proxy object that acts both as a Promise and an AsyncGenerator for backward compatibility
220
233
  // @ts-ignore - We're deliberately implementing a proxy with dual behavior
221
- return createBackwardCompatStreamingProxy(streamPromise);
234
+ return (0, api_core_1.createBackwardCompatStreamingProxy)(streamPromise);
222
235
  }
223
236
  /**
224
237
  * Buffer streaming results into a single response for cases where
@@ -233,157 +246,54 @@ async function bufferStreamingResults(prompt, options) {
233
246
  try {
234
247
  // Get streaming generator
235
248
  const generator = (await callAI(prompt, streamingOptions));
236
- // Buffer all chunks
237
- let finalResult = "";
238
- let chunkCount = 0;
239
- for await (const chunk of generator) {
240
- finalResult = chunk; // Each chunk contains the full accumulated text
241
- chunkCount++;
249
+ // For Claude JSON responses, take only the last chunk (the final processed result)
250
+ // For all other cases, concatenate chunks as before
251
+ const isClaudeJson = /claude/.test(options.model || "") && options.schema;
252
+ if (isClaudeJson) {
253
+ // For Claude with JSON schema, we only want the last yielded value
254
+ // which will be the complete, properly processed JSON
255
+ let lastChunk = "";
256
+ for await (const chunk of generator) {
257
+ // Replace the last chunk entirely instead of concatenating
258
+ lastChunk = chunk;
259
+ }
260
+ return lastChunk;
261
+ }
262
+ else {
263
+ // For all other cases, concatenate chunks
264
+ let result = "";
265
+ for await (const chunk of generator) {
266
+ result += chunk;
267
+ }
268
+ return result;
242
269
  }
243
- return finalResult;
244
270
  }
245
271
  catch (error) {
246
- handleApiError(error, "Streaming buffer error", options.debug);
272
+ // Handle errors with standard API error handling
273
+ await (0, error_handling_1.handleApiError)(error, "Buffered streaming", options.debug, {
274
+ apiKey: options.apiKey,
275
+ endpoint: options.endpoint,
276
+ skipRefresh: options.skipRefresh,
277
+ refreshToken: options.refreshToken,
278
+ updateRefreshToken: options.updateRefreshToken,
279
+ });
280
+ // If we get here, key was refreshed successfully, retry the operation with the new key
281
+ // Retry with the refreshed key
282
+ return bufferStreamingResults(prompt, {
283
+ ...options,
284
+ apiKey: key_management_1.keyStore.current || undefined, // Use the refreshed key from keyStore
285
+ });
247
286
  }
287
+ // This line should never be reached, but it satisfies the linter by ensuring
288
+ // all code paths return a value
289
+ throw new Error("Unexpected code path in bufferStreamingResults");
248
290
  }
249
291
  /**
250
292
  * Standardized API error handler
251
293
  */
252
- /**
253
- * Create a proxy that acts both as a Promise and an AsyncGenerator for backward compatibility
254
- * @internal This is for internal use only, not part of public API
255
- */
256
- function createBackwardCompatStreamingProxy(promise) {
257
- // Create a proxy that forwards methods to the Promise or AsyncGenerator as appropriate
258
- return new Proxy({}, {
259
- get(target, prop) {
260
- // First check if it's an AsyncGenerator method (needed for for-await)
261
- if (prop === "next" ||
262
- prop === "throw" ||
263
- prop === "return" ||
264
- prop === Symbol.asyncIterator) {
265
- // Create wrapper functions that await the Promise first
266
- if (prop === Symbol.asyncIterator) {
267
- return function () {
268
- return {
269
- // Implement async iterator that gets the generator first
270
- async next(value) {
271
- try {
272
- const generator = await promise;
273
- return generator.next(value);
274
- }
275
- catch (error) {
276
- // Turn Promise rejection into iterator result with error thrown
277
- return Promise.reject(error);
278
- }
279
- },
280
- };
281
- };
282
- }
283
- // Methods like next, throw, return
284
- return async function (value) {
285
- const generator = await promise;
286
- return generator[prop](value);
287
- };
288
- }
289
- // Then check if it's a Promise method
290
- if (prop === "then" || prop === "catch" || prop === "finally") {
291
- return promise[prop].bind(promise);
292
- }
293
- return undefined;
294
- },
295
- });
296
- }
297
- /**
298
- * Standardized API error handler
299
- */
300
- function handleApiError(error, context, debug = false) {
301
- if (debug) {
302
- console.error(`[callAI:${context}]:`, error);
303
- }
304
- throw new Error(`${context}: ${String(error)}`);
305
- }
306
- /**
307
- * Helper to check if an error indicates invalid model and handle fallback
308
- */
309
- async function checkForInvalidModelError(response, model, isRetry, skipRetry = false, debug = false) {
310
- // Skip retry immediately if skipRetry is true or if we're already retrying
311
- if (skipRetry || isRetry) {
312
- return { isInvalidModel: false };
313
- }
314
- // We want to check all 4xx errors, not just 400
315
- if (response.status < 400 || response.status >= 500) {
316
- return { isInvalidModel: false };
317
- }
318
- // Clone the response so we can read the body
319
- const clonedResponse = response.clone();
320
- try {
321
- const errorData = await clonedResponse.json();
322
- if (debug) {
323
- console.log(`[callAI:${PACKAGE_VERSION}] Checking for invalid model error:`, {
324
- model,
325
- statusCode: response.status,
326
- errorData,
327
- });
328
- }
329
- // Common patterns for invalid model errors across different providers
330
- const invalidModelPatterns = [
331
- "not a valid model",
332
- "model .* does not exist",
333
- "invalid model",
334
- "unknown model",
335
- "no provider was found",
336
- "fake-model", // For our test case
337
- "does-not-exist", // For our test case
338
- ];
339
- // Check if error message contains any of our patterns
340
- let errorMessage = "";
341
- if (errorData.error && errorData.error.message) {
342
- errorMessage = errorData.error.message.toLowerCase();
343
- }
344
- else if (errorData.message) {
345
- errorMessage = errorData.message.toLowerCase();
346
- }
347
- else {
348
- errorMessage = JSON.stringify(errorData).toLowerCase();
349
- }
350
- // Test the error message against each pattern
351
- const isInvalidModel = invalidModelPatterns.some((pattern) => errorMessage.includes(pattern.toLowerCase()));
352
- if (isInvalidModel && debug) {
353
- console.warn(`[callAI:${PACKAGE_VERSION}] Model ${model} not valid, will retry with ${FALLBACK_MODEL}`);
354
- }
355
- return { isInvalidModel, errorData };
356
- }
357
- catch (parseError) {
358
- // If we can't parse the response as JSON, try to read it as text
359
- if (debug) {
360
- console.error("Failed to parse error response as JSON:", parseError);
361
- }
362
- try {
363
- const textResponse = await response.clone().text();
364
- if (debug) {
365
- console.log("Error response as text:", textResponse);
366
- }
367
- // Even if it's not JSON, check if it contains any of our known patterns
368
- const lowerText = textResponse.toLowerCase();
369
- const isInvalidModel = lowerText.includes("invalid model") ||
370
- lowerText.includes("not exist") ||
371
- lowerText.includes("fake-model");
372
- if (isInvalidModel) {
373
- if (debug) {
374
- console.warn(`[callAI:${PACKAGE_VERSION}] Detected invalid model in text response for ${model}`);
375
- }
376
- }
377
- return { isInvalidModel, errorData: { text: textResponse } };
378
- }
379
- catch (textError) {
380
- if (debug) {
381
- console.error("Failed to read error response as text:", textError);
382
- }
383
- return { isInvalidModel: false };
384
- }
385
- }
386
- }
294
+ // createBackwardCompatStreamingProxy is imported from api-core.ts
295
+ // handleApiError is imported from error-handling.ts
296
+ // checkForInvalidModelError is imported from error-handling.ts
387
297
  /**
388
298
  * Prepare request parameters common to both streaming and non-streaming calls
389
299
  */
@@ -397,45 +307,62 @@ function prepareRequestParams(prompt, options) {
397
307
  // Select the appropriate strategy based on model and schema
398
308
  const schemaStrategy = (0, strategies_1.chooseSchemaStrategy)(options.model, schema);
399
309
  const model = schemaStrategy.model;
400
- const endpoint = options.endpoint || "https://openrouter.ai/api/v1/chat/completions";
310
+ // Get custom chat API origin if set
311
+ const customChatOrigin = options.chatUrl ||
312
+ (typeof window !== "undefined" ? window.CALLAI_CHAT_URL : null) ||
313
+ (typeof process !== "undefined" && process.env
314
+ ? process.env.CALLAI_CHAT_URL
315
+ : null);
316
+ // Use custom origin or default OpenRouter URL
317
+ const endpoint = options.endpoint ||
318
+ (customChatOrigin
319
+ ? `${customChatOrigin}/api/v1/chat/completions`
320
+ : "https://openrouter.ai/api/v1/chat/completions");
401
321
  // Handle both string prompts and message arrays for backward compatibility
402
322
  const messages = Array.isArray(prompt)
403
323
  ? prompt
404
324
  : [{ role: "user", content: prompt }];
405
- // Build request parameters
325
+ // Common parameters for both streaming and non-streaming
406
326
  const requestParams = {
407
- model: model,
408
- stream: options.stream === true,
409
- messages: messages,
327
+ model,
328
+ messages,
329
+ temperature: options.temperature !== undefined ? options.temperature : 0.7,
330
+ top_p: options.topP !== undefined ? options.topP : 1,
331
+ max_tokens: options.maxTokens || 2048,
332
+ stream: options.stream !== undefined ? options.stream : false,
410
333
  };
411
- // Support for multimodal content (like images)
412
- if (options.modalities && options.modalities.length > 0) {
413
- requestParams.modalities = options.modalities;
334
+ // Add optional parameters if specified
335
+ if (options.stop) {
336
+ // Handle both single string and array of stop sequences
337
+ requestParams.stop = Array.isArray(options.stop)
338
+ ? options.stop
339
+ : [options.stop];
414
340
  }
415
- // Apply the strategy's request preparation
416
- const strategyParams = schemaStrategy.prepareRequest(schema, messages);
417
- // If the strategy returns custom messages, use those instead
418
- if (strategyParams.messages) {
419
- requestParams.messages = strategyParams.messages;
341
+ // Add response_format parameter for models that support JSON output
342
+ if (options.responseFormat === "json") {
343
+ requestParams.response_format = { type: "json_object" };
420
344
  }
421
- // Add all other strategy parameters
422
- Object.entries(strategyParams).forEach(([key, value]) => {
423
- if (key !== "messages") {
424
- requestParams[key] = value;
425
- }
426
- });
427
- // Add any other options provided, but exclude internal keys
428
- Object.entries(options).forEach(([key, value]) => {
429
- if (!["apiKey", "model", "endpoint", "stream", "schema"].includes(key)) {
430
- requestParams[key] = value;
431
- }
432
- });
345
+ // Add schema structure if provided (for function calling/JSON mode)
346
+ if (schema) {
347
+ // Apply schema-specific parameters using the selected strategy
348
+ Object.assign(requestParams, schemaStrategy.prepareRequest(schema, messages));
349
+ }
350
+ // HTTP headers for the request
351
+ const headers = {
352
+ Authorization: `Bearer ${apiKey}`,
353
+ "Content-Type": "application/json",
354
+ "HTTP-Referer": options.referer || "https://vibes.diy",
355
+ "X-Title": options.title || "Vibes",
356
+ };
357
+ // Add any additional headers
358
+ if (options.headers) {
359
+ Object.assign(headers, options.headers);
360
+ }
361
+ // Build the requestOptions object for fetch
433
362
  const requestOptions = {
434
363
  method: "POST",
435
364
  headers: {
436
- Authorization: `Bearer ${apiKey}`,
437
- "HTTP-Referer": "https://vibes.diy",
438
- "X-Title": "Vibes",
365
+ ...headers,
439
366
  "Content-Type": "application/json",
440
367
  },
441
368
  body: JSON.stringify(requestParams),
@@ -453,25 +380,40 @@ function prepareRequestParams(prompt, options) {
453
380
  */
454
381
  async function callAINonStreaming(prompt, options = {}, isRetry = false) {
455
382
  try {
383
+ // Start timing for metadata
384
+ const startTime = Date.now();
385
+ // Create metadata object
386
+ const meta = {
387
+ model: options.model || "unknown",
388
+ timing: {
389
+ startTime: startTime,
390
+ },
391
+ };
456
392
  const { endpoint, requestOptions, model, schemaStrategy } = prepareRequestParams(prompt, options);
457
393
  const response = await fetch(endpoint, requestOptions);
394
+ // We don't store the raw Response object in metadata anymore
458
395
  // Handle HTTP errors, with potential fallback for invalid model
459
396
  if (!response.ok || response.status >= 400) {
460
- const { isInvalidModel } = await checkForInvalidModelError(response, model, isRetry, options.skipRetry, options.debug);
397
+ const { isInvalidModel } = await (0, error_handling_1.checkForInvalidModelError)(response, model, options.debug);
461
398
  if (isInvalidModel) {
462
399
  // Retry with fallback model
463
400
  return callAINonStreaming(prompt, { ...options, model: FALLBACK_MODEL }, true);
464
401
  }
465
- throw new Error(`HTTP error! Status: ${response.status}`);
402
+ // Create a proper error object with the status code preserved
403
+ const error = new Error(`HTTP error! Status: ${response.status}`);
404
+ // Add status code as a property of the error object
405
+ error.status = response.status;
406
+ error.statusCode = response.status; // Add statusCode for compatibility with different error patterns
407
+ throw error;
466
408
  }
467
409
  let result;
468
410
  // For Claude, use text() instead of json() to avoid potential hanging
469
411
  if (/claude/i.test(model)) {
470
412
  try {
471
- result = await extractClaudeResponse(response);
413
+ result = await (0, non_streaming_1.extractClaudeResponse)(response);
472
414
  }
473
415
  catch (error) {
474
- handleApiError(error, "Claude API response processing failed", options.debug);
416
+ (0, error_handling_1.handleApiError)(error, "Claude API response processing failed", options.debug);
475
417
  }
476
418
  }
477
419
  else {
@@ -502,309 +444,41 @@ async function callAINonStreaming(prompt, options = {}, isRetry = false) {
502
444
  });
503
445
  }
504
446
  // Extract content from the response
505
- const content = extractContent(result, schemaStrategy);
506
- // Process the content based on model type
507
- return schemaStrategy.processResponse(content);
508
- }
509
- catch (error) {
510
- handleApiError(error, "Non-streaming API call", options.debug);
511
- }
512
- }
513
- /**
514
- * Extract content from API response accounting for different formats
515
- */
516
- function extractContent(result, schemaStrategy) {
517
- // Find tool use content or normal content
518
- let content;
519
- // Extract tool use content if necessary
520
- if (schemaStrategy.strategy === "tool_mode" &&
521
- result.stop_reason === "tool_use") {
522
- // Try to find tool_use block in different response formats
523
- if (result.content && Array.isArray(result.content)) {
524
- const toolUseBlock = result.content.find((block) => block.type === "tool_use");
525
- if (toolUseBlock) {
526
- content = toolUseBlock;
527
- }
528
- }
529
- if (!content && result.choices && Array.isArray(result.choices)) {
530
- const choice = result.choices[0];
531
- if (choice.message && Array.isArray(choice.message.content)) {
532
- const toolUseBlock = choice.message.content.find((block) => block.type === "tool_use");
533
- if (toolUseBlock) {
534
- content = toolUseBlock;
535
- }
536
- }
447
+ const content = (0, non_streaming_1.extractContent)(result, schemaStrategy);
448
+ // Store the raw response data for user access
449
+ if (result) {
450
+ // Store the parsed JSON result from the API call
451
+ meta.rawResponse = result;
537
452
  }
538
- }
539
- // If no tool use content was found, use the standard message content
540
- if (!content) {
541
- if (!result.choices || !result.choices.length) {
542
- throw new Error("Invalid response format from API");
453
+ // Update model info
454
+ meta.model = model;
455
+ // Update timing info
456
+ if (meta.timing) {
457
+ meta.timing.endTime = Date.now();
458
+ meta.timing.duration = meta.timing.endTime - meta.timing.startTime;
543
459
  }
544
- content = result.choices[0]?.message?.content || "";
545
- }
546
- return content;
547
- }
548
- /**
549
- * Extract response from Claude API with timeout handling
550
- */
551
- async function extractClaudeResponse(response) {
552
- let textResponse;
553
- const textPromise = response.text();
554
- const timeoutPromise = new Promise((_resolve, reject) => {
555
- setTimeout(() => {
556
- reject(new Error("Text extraction timed out after 5 seconds"));
557
- }, 5000);
558
- });
559
- try {
560
- textResponse = (await Promise.race([
561
- textPromise,
562
- timeoutPromise,
563
- ]));
564
- }
565
- catch (textError) {
566
- // Always log timeout errors
567
- console.error(`Text extraction timed out or failed:`, textError);
568
- throw new Error("Claude response text extraction timed out. This is likely an issue with the Claude API's response format.");
569
- }
570
- try {
571
- return JSON.parse(textResponse);
572
- }
573
- catch (err) {
574
- // Always log JSON parsing errors
575
- console.error(`Failed to parse Claude response as JSON:`, err);
576
- throw new Error(`Failed to parse Claude response as JSON: ${err}`);
577
- }
578
- }
579
- /**
580
- * Generator factory function for streaming API calls
581
- * This is called after the fetch is made and response is validated
582
- *
583
- * Note: Even though we checked response.ok before creating this generator,
584
- * we need to be prepared for errors that may occur during streaming. Some APIs
585
- * return a 200 OK initially but then deliver error information in the stream.
586
- */
587
- async function* createStreamingGenerator(response, options, schemaStrategy, model) {
588
- if (options.debug) {
589
- console.log(`[callAI:${PACKAGE_VERSION}] Starting streaming generator with model: ${model}`);
590
- console.log(`[callAI:${PACKAGE_VERSION}] Response status:`, response.status);
591
- console.log(`[callAI:${PACKAGE_VERSION}] Response type:`, response.type);
592
- console.log(`[callAI:${PACKAGE_VERSION}] Response Content-Type:`, response.headers.get("content-type"));
593
- }
594
- try {
595
- // Handle streaming response
596
- if (!response.body) {
597
- throw new Error("Response body is undefined - API endpoint may not support streaming");
598
- }
599
- const reader = response.body.getReader();
600
- const decoder = new TextDecoder();
601
- let completeText = "";
602
- let chunkCount = 0;
603
- let toolCallsAssembled = "";
604
- while (true) {
605
- const { done, value } = await reader.read();
606
- if (done) {
607
- if (options.debug) {
608
- console.log(`[callAI:${PACKAGE_VERSION}] Stream done=true after ${chunkCount} chunks`);
609
- console.log(`[callAI-streaming:complete v${PACKAGE_VERSION}] Stream finished after ${chunkCount} chunks`);
610
- }
611
- break;
612
- }
613
- // Increment chunk counter before processing
614
- chunkCount++;
615
- const chunk = decoder.decode(value);
616
- if (options.debug) {
617
- console.log(`[callAI:${PACKAGE_VERSION}] Raw chunk #${chunkCount} (${chunk.length} bytes):`, chunk.length > 200 ? chunk.substring(0, 200) + "..." : chunk);
618
- }
619
- const lines = chunk.split("\n").filter((line) => line.trim() !== "");
620
- if (options.debug) {
621
- console.log(`[callAI:${PACKAGE_VERSION}] Chunk #${chunkCount} contains ${lines.length} non-empty lines`);
622
- }
623
- for (const line of lines) {
624
- if (options.debug) {
625
- console.log(`[callAI:${PACKAGE_VERSION}] Processing line:`, line.length > 100 ? line.substring(0, 100) + "..." : line);
626
- }
627
- if (line.startsWith("data: ")) {
628
- let data = line.slice(6);
629
- if (data === "[DONE]") {
630
- if (options.debug) {
631
- console.log(`[callAI:${PACKAGE_VERSION}] Received [DONE] marker`);
632
- }
633
- break;
634
- }
635
- if (options.debug) {
636
- console.log(`[callAI:raw] ${line}`);
637
- }
638
- // Skip [DONE] marker or OPENROUTER PROCESSING lines
639
- if (line.includes("[DONE]") ||
640
- line.includes("OPENROUTER PROCESSING")) {
641
- continue;
642
- }
643
- try {
644
- const jsonLine = line.replace("data: ", "");
645
- if (!jsonLine.trim()) {
646
- if (options.debug) {
647
- console.log(`[callAI:${PACKAGE_VERSION}] Empty JSON line after data: prefix`);
648
- }
649
- continue;
650
- }
651
- if (options.debug) {
652
- console.log(`[callAI:${PACKAGE_VERSION}] JSON line (first 100 chars):`, jsonLine.length > 100
653
- ? jsonLine.substring(0, 100) + "..."
654
- : jsonLine);
655
- }
656
- // Parse the JSON chunk
657
- let json;
658
- try {
659
- json = JSON.parse(jsonLine);
660
- if (options.debug) {
661
- console.log(`[callAI:${PACKAGE_VERSION}] Parsed JSON:`, JSON.stringify(json).substring(0, 1000));
662
- }
663
- }
664
- catch (parseError) {
665
- if (options.debug) {
666
- console.error(`[callAI:${PACKAGE_VERSION}] JSON parse error:`, parseError);
667
- }
668
- continue;
669
- }
670
- // Enhanced error detection - check for BOTH error and json.error
671
- // Some APIs return 200 OK but then deliver errors in the stream
672
- if (json.error || (typeof json === "object" && "error" in json)) {
673
- if (options.debug) {
674
- console.error(`[callAI:${PACKAGE_VERSION}] Detected error in streaming response:`, json);
675
- }
676
- // Create a detailed error object similar to our HTTP error handling
677
- const errorMessage = json.error?.message ||
678
- json.error?.toString() ||
679
- JSON.stringify(json.error || json);
680
- const detailedError = new Error(`API streaming error: ${errorMessage}`);
681
- // Add error metadata
682
- detailedError.status = json.error?.status || 400;
683
- detailedError.statusText =
684
- json.error?.type || "Bad Request";
685
- detailedError.details = JSON.stringify(json.error || json);
686
- console.error(`[callAI:${PACKAGE_VERSION}] Throwing stream error:`, detailedError);
687
- throw detailedError;
688
- }
689
- // Handle tool use response - Claude with schema cases
690
- const isClaudeWithSchema = /claude/i.test(model) && schemaStrategy.strategy === "tool_mode";
691
- if (isClaudeWithSchema) {
692
- // Claude streaming tool calls - need to assemble arguments
693
- if (json.choices && json.choices.length > 0) {
694
- const choice = json.choices[0];
695
- // Handle finish reason tool_calls
696
- if (choice.finish_reason === "tool_calls") {
697
- try {
698
- // Parse the assembled JSON
699
- completeText = toolCallsAssembled;
700
- yield completeText;
701
- continue;
702
- }
703
- catch (e) {
704
- console.error("[callAIStreaming] Error parsing assembled tool call:", e);
705
- }
706
- }
707
- // Assemble tool_calls arguments from delta
708
- if (choice.delta && choice.delta.tool_calls) {
709
- const toolCall = choice.delta.tool_calls[0];
710
- if (toolCall &&
711
- toolCall.function &&
712
- toolCall.function.arguments !== undefined) {
713
- toolCallsAssembled += toolCall.function.arguments;
714
- // We don't yield here to avoid partial JSON
715
- }
716
- }
717
- }
718
- }
719
- // Handle tool use response - old format
720
- if (isClaudeWithSchema &&
721
- (json.stop_reason === "tool_use" || json.type === "tool_use")) {
722
- // First try direct tool use object format
723
- if (json.type === "tool_use") {
724
- completeText = schemaStrategy.processResponse(json);
725
- yield completeText;
726
- continue;
727
- }
728
- // Extract the tool use content
729
- if (json.content && Array.isArray(json.content)) {
730
- const toolUseBlock = json.content.find((block) => block.type === "tool_use");
731
- if (toolUseBlock) {
732
- completeText = schemaStrategy.processResponse(toolUseBlock);
733
- yield completeText;
734
- continue;
735
- }
736
- }
737
- // Find tool_use in assistant's content blocks
738
- if (json.choices && Array.isArray(json.choices)) {
739
- const choice = json.choices[0];
740
- if (choice.message && Array.isArray(choice.message.content)) {
741
- const toolUseBlock = choice.message.content.find((block) => block.type === "tool_use");
742
- if (toolUseBlock) {
743
- completeText = schemaStrategy.processResponse(toolUseBlock);
744
- yield completeText;
745
- continue;
746
- }
747
- }
748
- // Handle case where the tool use is in the delta
749
- if (choice.delta && Array.isArray(choice.delta.content)) {
750
- const toolUseBlock = choice.delta.content.find((block) => block.type === "tool_use");
751
- if (toolUseBlock) {
752
- completeText = schemaStrategy.processResponse(toolUseBlock);
753
- yield completeText;
754
- continue;
755
- }
756
- }
757
- }
758
- }
759
- // Extract content from the delta
760
- if (json.choices?.[0]?.delta?.content !== undefined) {
761
- const content = json.choices[0].delta.content || "";
762
- // Treat all models the same - yield as content arrives
763
- completeText += content;
764
- yield schemaStrategy.processResponse(completeText);
765
- }
766
- // Handle message content format (non-streaming deltas)
767
- else if (json.choices?.[0]?.message?.content !== undefined) {
768
- const content = json.choices[0].message.content || "";
769
- completeText += content;
770
- yield schemaStrategy.processResponse(completeText);
771
- }
772
- // Handle content blocks for Claude/Anthropic response format
773
- else if (json.choices?.[0]?.message?.content &&
774
- Array.isArray(json.choices[0].message.content)) {
775
- const contentBlocks = json.choices[0].message.content;
776
- // Find text or tool_use blocks
777
- for (const block of contentBlocks) {
778
- if (block.type === "text") {
779
- completeText += block.text || "";
780
- }
781
- else if (isClaudeWithSchema && block.type === "tool_use") {
782
- completeText = schemaStrategy.processResponse(block);
783
- break; // We found what we need
784
- }
785
- }
786
- yield schemaStrategy.processResponse(completeText);
787
- }
788
- }
789
- catch (e) {
790
- if (options.debug) {
791
- console.error(`[callAIStreaming] Error parsing JSON chunk:`, e);
792
- }
793
- }
794
- }
795
- }
796
- }
797
- // We no longer need special error handling here as errors are thrown immediately
798
- // No extra error handling needed here - errors are thrown immediately
799
- // If we have assembled tool calls but haven't yielded them yet
800
- if (toolCallsAssembled && (!completeText || completeText.length === 0)) {
801
- return toolCallsAssembled;
802
- }
803
- // Ensure the final return has proper, processed content
804
- return schemaStrategy.processResponse(completeText);
460
+ // Process the content based on model type
461
+ const processedContent = schemaStrategy.processResponse(content);
462
+ // Box the string for WeakMap storage
463
+ const boxed = (0, response_metadata_1.boxString)(processedContent);
464
+ response_metadata_1.responseMetadata.set(boxed, meta);
465
+ return processedContent;
805
466
  }
806
467
  catch (error) {
807
- // Standardize error handling
808
- handleApiError(error, "Streaming API call", options.debug);
468
+ await (0, error_handling_1.handleApiError)(error, "Non-streaming API call", options.debug, {
469
+ apiKey: options.apiKey,
470
+ endpoint: options.endpoint,
471
+ skipRefresh: options.skipRefresh,
472
+ refreshToken: options.refreshToken,
473
+ updateRefreshToken: options.updateRefreshToken,
474
+ });
475
+ // If we get here, key was refreshed successfully, retry the operation with the new key
476
+ // Retry with the refreshed key
477
+ return callAINonStreaming(prompt, {
478
+ ...options,
479
+ apiKey: key_management_1.keyStore.current || undefined, // Use the refreshed key from keyStore
480
+ }, true); // Set isRetry to true
809
481
  }
482
+ // This line will never be reached, but it satisfies the linter
483
+ throw new Error("Unexpected code path in callAINonStreaming");
810
484
  }