call-ai 0.8.4 → 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-core.d.ts +40 -0
- package/dist/api-core.js +313 -0
- package/dist/api.d.ts +2 -0
- package/dist/api.js +148 -480
- package/dist/error-handling.d.ts +12 -0
- package/dist/error-handling.js +176 -0
- package/dist/image.js +9 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/key-management.d.ts +43 -0
- package/dist/key-management.js +312 -0
- package/dist/non-streaming.d.ts +10 -0
- package/dist/non-streaming.js +265 -0
- package/dist/response-metadata.d.ts +18 -0
- package/dist/response-metadata.js +44 -0
- package/dist/strategies/model-strategies.js +2 -2
- package/dist/streaming.d.ts +7 -0
- package/dist/streaming.js +483 -0
- package/dist/types.d.ts +40 -0
- package/package.json +12 -13
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
*/
|
|
@@ -400,48 +310,59 @@ function prepareRequestParams(prompt, options) {
|
|
|
400
310
|
// Get custom chat API origin if set
|
|
401
311
|
const customChatOrigin = options.chatUrl ||
|
|
402
312
|
(typeof window !== "undefined" ? window.CALLAI_CHAT_URL : null) ||
|
|
403
|
-
(typeof process !== "undefined" && process.env
|
|
313
|
+
(typeof process !== "undefined" && process.env
|
|
314
|
+
? process.env.CALLAI_CHAT_URL
|
|
315
|
+
: null);
|
|
404
316
|
// Use custom origin or default OpenRouter URL
|
|
405
317
|
const endpoint = options.endpoint ||
|
|
406
|
-
(customChatOrigin
|
|
318
|
+
(customChatOrigin
|
|
319
|
+
? `${customChatOrigin}/api/v1/chat/completions`
|
|
320
|
+
: "https://openrouter.ai/api/v1/chat/completions");
|
|
407
321
|
// Handle both string prompts and message arrays for backward compatibility
|
|
408
322
|
const messages = Array.isArray(prompt)
|
|
409
323
|
? prompt
|
|
410
324
|
: [{ role: "user", content: prompt }];
|
|
411
|
-
//
|
|
325
|
+
// Common parameters for both streaming and non-streaming
|
|
412
326
|
const requestParams = {
|
|
413
|
-
model
|
|
414
|
-
|
|
415
|
-
|
|
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,
|
|
416
333
|
};
|
|
417
|
-
//
|
|
418
|
-
if (options.
|
|
419
|
-
|
|
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];
|
|
420
340
|
}
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (strategyParams.messages) {
|
|
425
|
-
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" };
|
|
426
344
|
}
|
|
427
|
-
// Add
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
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
|
|
439
362
|
const requestOptions = {
|
|
440
363
|
method: "POST",
|
|
441
364
|
headers: {
|
|
442
|
-
|
|
443
|
-
"HTTP-Referer": "https://vibes.diy",
|
|
444
|
-
"X-Title": "Vibes",
|
|
365
|
+
...headers,
|
|
445
366
|
"Content-Type": "application/json",
|
|
446
367
|
},
|
|
447
368
|
body: JSON.stringify(requestParams),
|
|
@@ -459,25 +380,40 @@ function prepareRequestParams(prompt, options) {
|
|
|
459
380
|
*/
|
|
460
381
|
async function callAINonStreaming(prompt, options = {}, isRetry = false) {
|
|
461
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
|
+
};
|
|
462
392
|
const { endpoint, requestOptions, model, schemaStrategy } = prepareRequestParams(prompt, options);
|
|
463
393
|
const response = await fetch(endpoint, requestOptions);
|
|
394
|
+
// We don't store the raw Response object in metadata anymore
|
|
464
395
|
// Handle HTTP errors, with potential fallback for invalid model
|
|
465
396
|
if (!response.ok || response.status >= 400) {
|
|
466
|
-
const { isInvalidModel } = await checkForInvalidModelError(response, model,
|
|
397
|
+
const { isInvalidModel } = await (0, error_handling_1.checkForInvalidModelError)(response, model, options.debug);
|
|
467
398
|
if (isInvalidModel) {
|
|
468
399
|
// Retry with fallback model
|
|
469
400
|
return callAINonStreaming(prompt, { ...options, model: FALLBACK_MODEL }, true);
|
|
470
401
|
}
|
|
471
|
-
|
|
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;
|
|
472
408
|
}
|
|
473
409
|
let result;
|
|
474
410
|
// For Claude, use text() instead of json() to avoid potential hanging
|
|
475
411
|
if (/claude/i.test(model)) {
|
|
476
412
|
try {
|
|
477
|
-
result = await extractClaudeResponse(response);
|
|
413
|
+
result = await (0, non_streaming_1.extractClaudeResponse)(response);
|
|
478
414
|
}
|
|
479
415
|
catch (error) {
|
|
480
|
-
handleApiError(error, "Claude API response processing failed", options.debug);
|
|
416
|
+
(0, error_handling_1.handleApiError)(error, "Claude API response processing failed", options.debug);
|
|
481
417
|
}
|
|
482
418
|
}
|
|
483
419
|
else {
|
|
@@ -508,309 +444,41 @@ async function callAINonStreaming(prompt, options = {}, isRetry = false) {
|
|
|
508
444
|
});
|
|
509
445
|
}
|
|
510
446
|
// Extract content from the response
|
|
511
|
-
const content = extractContent(result, schemaStrategy);
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
handleApiError(error, "Non-streaming API call", options.debug);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
/**
|
|
520
|
-
* Extract content from API response accounting for different formats
|
|
521
|
-
*/
|
|
522
|
-
function extractContent(result, schemaStrategy) {
|
|
523
|
-
// Find tool use content or normal content
|
|
524
|
-
let content;
|
|
525
|
-
// Extract tool use content if necessary
|
|
526
|
-
if (schemaStrategy.strategy === "tool_mode" &&
|
|
527
|
-
result.stop_reason === "tool_use") {
|
|
528
|
-
// Try to find tool_use block in different response formats
|
|
529
|
-
if (result.content && Array.isArray(result.content)) {
|
|
530
|
-
const toolUseBlock = result.content.find((block) => block.type === "tool_use");
|
|
531
|
-
if (toolUseBlock) {
|
|
532
|
-
content = toolUseBlock;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
if (!content && result.choices && Array.isArray(result.choices)) {
|
|
536
|
-
const choice = result.choices[0];
|
|
537
|
-
if (choice.message && Array.isArray(choice.message.content)) {
|
|
538
|
-
const toolUseBlock = choice.message.content.find((block) => block.type === "tool_use");
|
|
539
|
-
if (toolUseBlock) {
|
|
540
|
-
content = toolUseBlock;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
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;
|
|
543
452
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (
|
|
548
|
-
|
|
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;
|
|
549
459
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
*/
|
|
557
|
-
async function extractClaudeResponse(response) {
|
|
558
|
-
let textResponse;
|
|
559
|
-
const textPromise = response.text();
|
|
560
|
-
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
561
|
-
setTimeout(() => {
|
|
562
|
-
reject(new Error("Text extraction timed out after 5 seconds"));
|
|
563
|
-
}, 5000);
|
|
564
|
-
});
|
|
565
|
-
try {
|
|
566
|
-
textResponse = (await Promise.race([
|
|
567
|
-
textPromise,
|
|
568
|
-
timeoutPromise,
|
|
569
|
-
]));
|
|
570
|
-
}
|
|
571
|
-
catch (textError) {
|
|
572
|
-
// Always log timeout errors
|
|
573
|
-
console.error(`Text extraction timed out or failed:`, textError);
|
|
574
|
-
throw new Error("Claude response text extraction timed out. This is likely an issue with the Claude API's response format.");
|
|
575
|
-
}
|
|
576
|
-
try {
|
|
577
|
-
return JSON.parse(textResponse);
|
|
578
|
-
}
|
|
579
|
-
catch (err) {
|
|
580
|
-
// Always log JSON parsing errors
|
|
581
|
-
console.error(`Failed to parse Claude response as JSON:`, err);
|
|
582
|
-
throw new Error(`Failed to parse Claude response as JSON: ${err}`);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
* Generator factory function for streaming API calls
|
|
587
|
-
* This is called after the fetch is made and response is validated
|
|
588
|
-
*
|
|
589
|
-
* Note: Even though we checked response.ok before creating this generator,
|
|
590
|
-
* we need to be prepared for errors that may occur during streaming. Some APIs
|
|
591
|
-
* return a 200 OK initially but then deliver error information in the stream.
|
|
592
|
-
*/
|
|
593
|
-
async function* createStreamingGenerator(response, options, schemaStrategy, model) {
|
|
594
|
-
if (options.debug) {
|
|
595
|
-
console.log(`[callAI:${PACKAGE_VERSION}] Starting streaming generator with model: ${model}`);
|
|
596
|
-
console.log(`[callAI:${PACKAGE_VERSION}] Response status:`, response.status);
|
|
597
|
-
console.log(`[callAI:${PACKAGE_VERSION}] Response type:`, response.type);
|
|
598
|
-
console.log(`[callAI:${PACKAGE_VERSION}] Response Content-Type:`, response.headers.get("content-type"));
|
|
599
|
-
}
|
|
600
|
-
try {
|
|
601
|
-
// Handle streaming response
|
|
602
|
-
if (!response.body) {
|
|
603
|
-
throw new Error("Response body is undefined - API endpoint may not support streaming");
|
|
604
|
-
}
|
|
605
|
-
const reader = response.body.getReader();
|
|
606
|
-
const decoder = new TextDecoder();
|
|
607
|
-
let completeText = "";
|
|
608
|
-
let chunkCount = 0;
|
|
609
|
-
let toolCallsAssembled = "";
|
|
610
|
-
while (true) {
|
|
611
|
-
const { done, value } = await reader.read();
|
|
612
|
-
if (done) {
|
|
613
|
-
if (options.debug) {
|
|
614
|
-
console.log(`[callAI:${PACKAGE_VERSION}] Stream done=true after ${chunkCount} chunks`);
|
|
615
|
-
console.log(`[callAI-streaming:complete v${PACKAGE_VERSION}] Stream finished after ${chunkCount} chunks`);
|
|
616
|
-
}
|
|
617
|
-
break;
|
|
618
|
-
}
|
|
619
|
-
// Increment chunk counter before processing
|
|
620
|
-
chunkCount++;
|
|
621
|
-
const chunk = decoder.decode(value);
|
|
622
|
-
if (options.debug) {
|
|
623
|
-
console.log(`[callAI:${PACKAGE_VERSION}] Raw chunk #${chunkCount} (${chunk.length} bytes):`, chunk.length > 200 ? chunk.substring(0, 200) + "..." : chunk);
|
|
624
|
-
}
|
|
625
|
-
const lines = chunk.split("\n").filter((line) => line.trim() !== "");
|
|
626
|
-
if (options.debug) {
|
|
627
|
-
console.log(`[callAI:${PACKAGE_VERSION}] Chunk #${chunkCount} contains ${lines.length} non-empty lines`);
|
|
628
|
-
}
|
|
629
|
-
for (const line of lines) {
|
|
630
|
-
if (options.debug) {
|
|
631
|
-
console.log(`[callAI:${PACKAGE_VERSION}] Processing line:`, line.length > 100 ? line.substring(0, 100) + "..." : line);
|
|
632
|
-
}
|
|
633
|
-
if (line.startsWith("data: ")) {
|
|
634
|
-
let data = line.slice(6);
|
|
635
|
-
if (data === "[DONE]") {
|
|
636
|
-
if (options.debug) {
|
|
637
|
-
console.log(`[callAI:${PACKAGE_VERSION}] Received [DONE] marker`);
|
|
638
|
-
}
|
|
639
|
-
break;
|
|
640
|
-
}
|
|
641
|
-
if (options.debug) {
|
|
642
|
-
console.log(`[callAI:raw] ${line}`);
|
|
643
|
-
}
|
|
644
|
-
// Skip [DONE] marker or OPENROUTER PROCESSING lines
|
|
645
|
-
if (line.includes("[DONE]") ||
|
|
646
|
-
line.includes("OPENROUTER PROCESSING")) {
|
|
647
|
-
continue;
|
|
648
|
-
}
|
|
649
|
-
try {
|
|
650
|
-
const jsonLine = line.replace("data: ", "");
|
|
651
|
-
if (!jsonLine.trim()) {
|
|
652
|
-
if (options.debug) {
|
|
653
|
-
console.log(`[callAI:${PACKAGE_VERSION}] Empty JSON line after data: prefix`);
|
|
654
|
-
}
|
|
655
|
-
continue;
|
|
656
|
-
}
|
|
657
|
-
if (options.debug) {
|
|
658
|
-
console.log(`[callAI:${PACKAGE_VERSION}] JSON line (first 100 chars):`, jsonLine.length > 100
|
|
659
|
-
? jsonLine.substring(0, 100) + "..."
|
|
660
|
-
: jsonLine);
|
|
661
|
-
}
|
|
662
|
-
// Parse the JSON chunk
|
|
663
|
-
let json;
|
|
664
|
-
try {
|
|
665
|
-
json = JSON.parse(jsonLine);
|
|
666
|
-
if (options.debug) {
|
|
667
|
-
console.log(`[callAI:${PACKAGE_VERSION}] Parsed JSON:`, JSON.stringify(json).substring(0, 1000));
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
catch (parseError) {
|
|
671
|
-
if (options.debug) {
|
|
672
|
-
console.error(`[callAI:${PACKAGE_VERSION}] JSON parse error:`, parseError);
|
|
673
|
-
}
|
|
674
|
-
continue;
|
|
675
|
-
}
|
|
676
|
-
// Enhanced error detection - check for BOTH error and json.error
|
|
677
|
-
// Some APIs return 200 OK but then deliver errors in the stream
|
|
678
|
-
if (json.error || (typeof json === "object" && "error" in json)) {
|
|
679
|
-
if (options.debug) {
|
|
680
|
-
console.error(`[callAI:${PACKAGE_VERSION}] Detected error in streaming response:`, json);
|
|
681
|
-
}
|
|
682
|
-
// Create a detailed error object similar to our HTTP error handling
|
|
683
|
-
const errorMessage = json.error?.message ||
|
|
684
|
-
json.error?.toString() ||
|
|
685
|
-
JSON.stringify(json.error || json);
|
|
686
|
-
const detailedError = new Error(`API streaming error: ${errorMessage}`);
|
|
687
|
-
// Add error metadata
|
|
688
|
-
detailedError.status = json.error?.status || 400;
|
|
689
|
-
detailedError.statusText =
|
|
690
|
-
json.error?.type || "Bad Request";
|
|
691
|
-
detailedError.details = JSON.stringify(json.error || json);
|
|
692
|
-
console.error(`[callAI:${PACKAGE_VERSION}] Throwing stream error:`, detailedError);
|
|
693
|
-
throw detailedError;
|
|
694
|
-
}
|
|
695
|
-
// Handle tool use response - Claude with schema cases
|
|
696
|
-
const isClaudeWithSchema = /claude/i.test(model) && schemaStrategy.strategy === "tool_mode";
|
|
697
|
-
if (isClaudeWithSchema) {
|
|
698
|
-
// Claude streaming tool calls - need to assemble arguments
|
|
699
|
-
if (json.choices && json.choices.length > 0) {
|
|
700
|
-
const choice = json.choices[0];
|
|
701
|
-
// Handle finish reason tool_calls
|
|
702
|
-
if (choice.finish_reason === "tool_calls") {
|
|
703
|
-
try {
|
|
704
|
-
// Parse the assembled JSON
|
|
705
|
-
completeText = toolCallsAssembled;
|
|
706
|
-
yield completeText;
|
|
707
|
-
continue;
|
|
708
|
-
}
|
|
709
|
-
catch (e) {
|
|
710
|
-
console.error("[callAIStreaming] Error parsing assembled tool call:", e);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
// Assemble tool_calls arguments from delta
|
|
714
|
-
if (choice.delta && choice.delta.tool_calls) {
|
|
715
|
-
const toolCall = choice.delta.tool_calls[0];
|
|
716
|
-
if (toolCall &&
|
|
717
|
-
toolCall.function &&
|
|
718
|
-
toolCall.function.arguments !== undefined) {
|
|
719
|
-
toolCallsAssembled += toolCall.function.arguments;
|
|
720
|
-
// We don't yield here to avoid partial JSON
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
// Handle tool use response - old format
|
|
726
|
-
if (isClaudeWithSchema &&
|
|
727
|
-
(json.stop_reason === "tool_use" || json.type === "tool_use")) {
|
|
728
|
-
// First try direct tool use object format
|
|
729
|
-
if (json.type === "tool_use") {
|
|
730
|
-
completeText = schemaStrategy.processResponse(json);
|
|
731
|
-
yield completeText;
|
|
732
|
-
continue;
|
|
733
|
-
}
|
|
734
|
-
// Extract the tool use content
|
|
735
|
-
if (json.content && Array.isArray(json.content)) {
|
|
736
|
-
const toolUseBlock = json.content.find((block) => block.type === "tool_use");
|
|
737
|
-
if (toolUseBlock) {
|
|
738
|
-
completeText = schemaStrategy.processResponse(toolUseBlock);
|
|
739
|
-
yield completeText;
|
|
740
|
-
continue;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
// Find tool_use in assistant's content blocks
|
|
744
|
-
if (json.choices && Array.isArray(json.choices)) {
|
|
745
|
-
const choice = json.choices[0];
|
|
746
|
-
if (choice.message && Array.isArray(choice.message.content)) {
|
|
747
|
-
const toolUseBlock = choice.message.content.find((block) => block.type === "tool_use");
|
|
748
|
-
if (toolUseBlock) {
|
|
749
|
-
completeText = schemaStrategy.processResponse(toolUseBlock);
|
|
750
|
-
yield completeText;
|
|
751
|
-
continue;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
// Handle case where the tool use is in the delta
|
|
755
|
-
if (choice.delta && Array.isArray(choice.delta.content)) {
|
|
756
|
-
const toolUseBlock = choice.delta.content.find((block) => block.type === "tool_use");
|
|
757
|
-
if (toolUseBlock) {
|
|
758
|
-
completeText = schemaStrategy.processResponse(toolUseBlock);
|
|
759
|
-
yield completeText;
|
|
760
|
-
continue;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
// Extract content from the delta
|
|
766
|
-
if (json.choices?.[0]?.delta?.content !== undefined) {
|
|
767
|
-
const content = json.choices[0].delta.content || "";
|
|
768
|
-
// Treat all models the same - yield as content arrives
|
|
769
|
-
completeText += content;
|
|
770
|
-
yield schemaStrategy.processResponse(completeText);
|
|
771
|
-
}
|
|
772
|
-
// Handle message content format (non-streaming deltas)
|
|
773
|
-
else if (json.choices?.[0]?.message?.content !== undefined) {
|
|
774
|
-
const content = json.choices[0].message.content || "";
|
|
775
|
-
completeText += content;
|
|
776
|
-
yield schemaStrategy.processResponse(completeText);
|
|
777
|
-
}
|
|
778
|
-
// Handle content blocks for Claude/Anthropic response format
|
|
779
|
-
else if (json.choices?.[0]?.message?.content &&
|
|
780
|
-
Array.isArray(json.choices[0].message.content)) {
|
|
781
|
-
const contentBlocks = json.choices[0].message.content;
|
|
782
|
-
// Find text or tool_use blocks
|
|
783
|
-
for (const block of contentBlocks) {
|
|
784
|
-
if (block.type === "text") {
|
|
785
|
-
completeText += block.text || "";
|
|
786
|
-
}
|
|
787
|
-
else if (isClaudeWithSchema && block.type === "tool_use") {
|
|
788
|
-
completeText = schemaStrategy.processResponse(block);
|
|
789
|
-
break; // We found what we need
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
yield schemaStrategy.processResponse(completeText);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
catch (e) {
|
|
796
|
-
if (options.debug) {
|
|
797
|
-
console.error(`[callAIStreaming] Error parsing JSON chunk:`, e);
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
// We no longer need special error handling here as errors are thrown immediately
|
|
804
|
-
// No extra error handling needed here - errors are thrown immediately
|
|
805
|
-
// If we have assembled tool calls but haven't yielded them yet
|
|
806
|
-
if (toolCallsAssembled && (!completeText || completeText.length === 0)) {
|
|
807
|
-
return toolCallsAssembled;
|
|
808
|
-
}
|
|
809
|
-
// Ensure the final return has proper, processed content
|
|
810
|
-
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;
|
|
811
466
|
}
|
|
812
467
|
catch (error) {
|
|
813
|
-
|
|
814
|
-
|
|
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
|
|
815
481
|
}
|
|
482
|
+
// This line will never be reached, but it satisfies the linter
|
|
483
|
+
throw new Error("Unexpected code path in callAINonStreaming");
|
|
816
484
|
}
|