call-ai 2.0.3 → 2.0.7

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/index.d.ts CHANGED
@@ -2,6 +2,5 @@ export * from "./types.js";
2
2
  export { callAi } from "./api.js";
3
3
  export { callAi as callAI } from "./api.js";
4
4
  export { getMeta } from "./response-metadata.js";
5
- export { imageGen } from "./image.js";
6
5
  export { entriesHeaders, joinUrlParts } from "./utils.js";
7
6
  export { callAiEnv } from "./env.js";
package/index.js CHANGED
@@ -2,7 +2,6 @@ export * from "./types.js";
2
2
  export { callAi } from "./api.js";
3
3
  export { callAi as callAI } from "./api.js";
4
4
  export { getMeta } from "./response-metadata.js";
5
- export { imageGen } from "./image.js";
6
5
  export { entriesHeaders, joinUrlParts } from "./utils.js";
7
6
  export { callAiEnv } from "./env.js";
8
7
  //# sourceMappingURL=index.js.map
package/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../jsr/index.ts"],"names":[],"mappings":"AAKA,cAAc,YAAY,CAAC;AAG3B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,OAAO,EAAE,MAAM,IAAI,MAAM,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAGjD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../jsr/index.ts"],"names":[],"mappings":"AAKA,cAAc,YAAY,CAAC;AAG3B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,OAAO,EAAE,MAAM,IAAI,MAAM,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAIjD,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "call-ai",
3
- "version": "2.0.3",
3
+ "version": "2.0.7",
4
4
  "description": "Lightweight library for making AI API calls with streaming support",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/types.d.ts CHANGED
@@ -230,7 +230,7 @@ export interface ImageResponse {
230
230
  readonly revised_prompt?: string;
231
231
  }[];
232
232
  }
233
- export interface ImageGenOptions {
233
+ export interface ImgVibesOptions {
234
234
  readonly apiKey?: string;
235
235
  readonly model?: string;
236
236
  readonly size?: string;
@@ -242,5 +242,5 @@ export interface ImageGenOptions {
242
242
  readonly debug?: boolean;
243
243
  readonly mock?: Mocks;
244
244
  }
245
- export type ImageEditOptions = ImageGenOptions;
245
+ export type ImageEditOptions = ImgVibesOptions;
246
246
  export {};
package/api.ts.off DELETED
@@ -1,595 +0,0 @@
1
- /**
2
- * Core API implementation for call-ai
3
- */
4
- import { CallAIError, CallAIErrorParams, CallAIOptions, Message, ResponseMeta, SchemaStrategy, StreamResponse } from "./types.js";
5
- import { chooseSchemaStrategy } from "./strategies/index.js";
6
- import { responseMetadata, boxString } from "./response-metadata.js";
7
- import { keyStore, globalDebug } from "./key-management.js";
8
- import { handleApiError, checkForInvalidModelError } from "./error-handling.js";
9
- import { createBackwardCompatStreamingProxy } from "./api-core.js";
10
- import { extractContent, extractClaudeResponse } from "./non-streaming.js";
11
- import { createStreamingGenerator } from "./streaming.js";
12
- import { PACKAGE_VERSION } from "./version.js";
13
- import { callAiEnv, callAiFetch } from "./utils.js";
14
-
15
- // Key management is now imported from ./key-management
16
-
17
- // initKeyStore is imported from key-management.ts
18
- // No need to call initKeyStore() here as it's called on module load in key-management.ts
19
-
20
- // isNewKeyError is imported from key-management.ts
21
-
22
- // refreshApiKey is imported from key-management.ts
23
-
24
- // getHashFromKey is imported from key-management.ts
25
-
26
- // storeKeyMetadata is imported from key-management.ts
27
-
28
- // Response metadata is now imported from ./response-metadata
29
-
30
- // boxString and getMeta functions are now imported from ./response-metadata
31
- // Re-export getMeta to maintain backward compatibility
32
- // export { getMeta };
33
-
34
- // Import package version for debugging
35
-
36
- // Default fallback model when the primary model fails or is unavailable
37
- const FALLBACK_MODEL = "openrouter/auto";
38
-
39
- /**
40
- * Make an AI API call with the given options
41
- * @param prompt User prompt as string or an array of message objects
42
- * @param options Configuration options including optional schema for structured output
43
- * @returns A Promise that resolves to the complete response string when streaming is disabled,
44
- * or a Promise that resolves to an AsyncGenerator when streaming is enabled.
45
- * The AsyncGenerator yields partial responses as they arrive.
46
- */
47
- export function callAi(prompt: string | Message[], options: CallAIOptions = {}): Promise<string | StreamResponse> {
48
- // Check if we need to force streaming based on model strategy
49
- const schemaStrategy = chooseSchemaStrategy(options.model, options.schema || null);
50
-
51
- // We no longer set a default maxTokens
52
- // Will only include max_tokens in the request if explicitly set by the user
53
-
54
- // Handle special case: Claude with tools requires streaming
55
- if (!options.stream && schemaStrategy.shouldForceStream) {
56
- // Buffer streaming results into a single response
57
- return bufferStreamingResults(prompt, options);
58
- }
59
-
60
- // Handle normal non-streaming mode
61
- if (options.stream !== true) {
62
- return callAINonStreaming(prompt, options);
63
- }
64
-
65
- // Handle streaming mode - return a Promise that resolves to an AsyncGenerator
66
- // but also supports legacy non-awaited usage for backward compatibility
67
- const streamPromise = (async () => {
68
- // Do setup and validation before returning the generator
69
- const { endpoint, requestOptions, model, schemaStrategy } = prepareRequestParams(prompt, { ...options, stream: true });
70
-
71
- // Use either explicit debug option or global debug flag
72
- const debug = options.debug || globalDebug;
73
- if (debug) {
74
- console.log(`[callAi:${PACKAGE_VERSION}] Making fetch request to: ${endpoint}`);
75
- console.log(`[callAi:${PACKAGE_VERSION}] With model: ${model}`);
76
- console.log(`[callAi:${PACKAGE_VERSION}] Request headers:`, JSON.stringify(requestOptions.headers));
77
- }
78
-
79
- let response;
80
- try {
81
- response = await callAiFetch(options)(endpoint, requestOptions);
82
- if (options.debug) {
83
- console.log(`[callAi:${PACKAGE_VERSION}] Fetch completed with status:`, response.status, response.statusText);
84
-
85
- // Log all headers
86
- console.log(`[callAi:${PACKAGE_VERSION}] Response headers:`);
87
- response.headers.forEach((value, name) => {
88
- console.log(`[callAi:${PACKAGE_VERSION}] ${name}: ${value}`);
89
- });
90
-
91
- // Clone response for diagnostic purposes only
92
- const diagnosticResponse = response.clone();
93
- try {
94
- // Try to get the response as text for debugging
95
- const responseText = await diagnosticResponse.text();
96
- console.log(
97
- `[callAi:${PACKAGE_VERSION}] First 500 chars of response body:`,
98
- responseText.substring(0, 500) + (responseText.length > 500 ? "..." : ""),
99
- );
100
- } catch (e) {
101
- console.log(`[callAi:${PACKAGE_VERSION}] Could not read response body for diagnostics:`, e);
102
- }
103
- }
104
- } catch (fetchError) {
105
- if (options.debug) {
106
- console.error(`[callAi:${PACKAGE_VERSION}] Network error during fetch:`, fetchError);
107
- }
108
- throw fetchError; // Re-throw network errors
109
- }
110
-
111
- // Explicitly check for HTTP error status and log extensively if debug is enabled
112
- // Safe access to headers in case of mock environments
113
- const contentType = response?.headers?.get?.("content-type") || "";
114
-
115
- if (options.debug) {
116
- console.log(`[callAi:${PACKAGE_VERSION}] Response.ok =`, response.ok);
117
- console.log(`[callAi:${PACKAGE_VERSION}] Response.status =`, response.status);
118
- console.log(`[callAi:${PACKAGE_VERSION}] Response.statusText =`, response.statusText);
119
- console.log(`[callAi:${PACKAGE_VERSION}] Response.type =`, response.type);
120
- console.log(`[callAi:${PACKAGE_VERSION}] Content-Type =`, contentType);
121
- }
122
-
123
- // Browser-compatible error handling - must check BOTH status code AND content-type
124
- // Some browsers will report status 200 for SSE streams even when server returns 400
125
- const hasHttpError = !response.ok || response.status >= 400;
126
- const hasJsonError = contentType.includes("application/json");
127
-
128
- if (hasHttpError || hasJsonError) {
129
- if (options.debug) {
130
- console.log(
131
- `[callAi:${PACKAGE_VERSION}] ⚠️ Error detected - HTTP Status: ${response.status}, Content-Type: ${contentType}`,
132
- );
133
- }
134
-
135
- // Handle the error with fallback model if appropriate
136
- if (!options.skipRetry) {
137
- const clonedResponse = response.clone();
138
- let isInvalidModel = false;
139
-
140
- try {
141
- // Check if this is an invalid model error
142
- const modelCheckResult = await checkForInvalidModelError(clonedResponse, model, options.debug);
143
- isInvalidModel = modelCheckResult.isInvalidModel;
144
-
145
- if (isInvalidModel) {
146
- if (options.debug) {
147
- console.log(`[callAi:${PACKAGE_VERSION}] Retrying with fallback model: ${FALLBACK_MODEL}`);
148
- }
149
- // Retry with fallback model
150
- return (await callAi(prompt, {
151
- ...options,
152
- model: FALLBACK_MODEL,
153
- })) as StreamResponse;
154
- }
155
- } catch (modelCheckError) {
156
- console.error(`[callAi:${PACKAGE_VERSION}] Error during model check:`, modelCheckError);
157
- // Continue with normal error handling
158
- }
159
- }
160
-
161
- // Extract error details from response
162
- try {
163
- // Try to get error details from the response body
164
- const errorBody = await response.text();
165
- if (options.debug) {
166
- console.log(`[callAi:${PACKAGE_VERSION}] Error body:`, errorBody);
167
- }
168
-
169
- try {
170
- // Try to parse JSON error
171
- const errorJson = JSON.parse(errorBody);
172
- if (options.debug) {
173
- console.log(`[callAi:${PACKAGE_VERSION}] Parsed error:`, errorJson);
174
- }
175
-
176
- // Extract message from OpenRouter error format
177
- let errorMessage = "";
178
-
179
- // Handle common error formats
180
- if (errorJson.error && typeof errorJson.error === "object" && errorJson.error.message) {
181
- // OpenRouter/OpenAI format: { error: { message: "..." } }
182
- errorMessage = errorJson.error.message;
183
- } else if (errorJson.error && typeof errorJson.error === "string") {
184
- // Simple error format: { error: "..." }
185
- errorMessage = errorJson.error;
186
- } else if (errorJson.message) {
187
- // Generic format: { message: "..." }
188
- errorMessage = errorJson.message;
189
- } else {
190
- // Fallback with status details
191
- errorMessage = `API returned ${response.status}: ${response.statusText}`;
192
- }
193
-
194
- // Add status details to error message if not already included
195
- if (!errorMessage.includes(response.status.toString())) {
196
- errorMessage = `${errorMessage} (Status: ${response.status})`;
197
- }
198
-
199
- if (options.debug) {
200
- console.log(`[callAi:${PACKAGE_VERSION}] Extracted error message:`, errorMessage);
201
- }
202
-
203
- // Create error with standard format
204
- const error = new CallAIError({
205
- message: errorMessage,
206
- status: response.status,
207
- statusText: response.statusText,
208
- details: errorJson,
209
- contentType,
210
- });
211
- throw error;
212
- } catch (jsonError) {
213
- // If JSON parsing fails, extract a useful message from the raw error body
214
- if (options.debug) {
215
- console.log(`[callAi:${PACKAGE_VERSION}] JSON parse error:`, jsonError);
216
- }
217
-
218
- // Try to extract a useful message even from non-JSON text
219
- let errorMessage = "";
220
-
221
- // Check if it's a plain text error message
222
- if (errorBody && errorBody.trim().length > 0) {
223
- // Limit length for readability
224
- errorMessage = errorBody.length > 100 ? errorBody.substring(0, 100) + "..." : errorBody;
225
- } else {
226
- errorMessage = `API error: ${response.status} ${response.statusText}`;
227
- }
228
-
229
- // Add status details if not already included
230
- if (!errorMessage.includes(response.status.toString())) {
231
- errorMessage = `${errorMessage} (Status: ${response.status})`;
232
- }
233
-
234
- if (options.debug) {
235
- console.log(`[callAi:${PACKAGE_VERSION}] Extracted text error message:`, errorMessage);
236
- }
237
-
238
- const error = new CallAIError({
239
- message: errorMessage,
240
- status: response.status,
241
- statusText: response.statusText,
242
- details: errorBody,
243
- contentType,
244
- });
245
- throw error;
246
- }
247
- } catch (responseError) {
248
- if (responseError instanceof Error) {
249
- // Re-throw if it's already properly formatted
250
- throw responseError;
251
- }
252
-
253
- // Fallback error
254
- const error = new CallAIError({
255
- message: `API returned ${response.status}: ${response.statusText}`,
256
- status: response.status,
257
- statusText: response.statusText,
258
- details: undefined,
259
- contentType,
260
- });
261
- throw error;
262
- }
263
- }
264
- // Only if response is OK, create and return the streaming generator
265
- if (options.debug) {
266
- console.log(`[callAi:${PACKAGE_VERSION}] Response OK, creating streaming generator`);
267
- }
268
- return createStreamingGenerator(response, options, schemaStrategy, model);
269
- })();
270
-
271
- // For backward compatibility with v0.6.x where users didn't await the result
272
- if (callAiEnv.NODE_ENV !== "production") {
273
- if (options.debug) {
274
- console.warn(
275
- `[callAi:${PACKAGE_VERSION}] No await found - using legacy streaming pattern. This will be removed in a future version and may cause issues with certain models.`,
276
- );
277
- }
278
- }
279
-
280
- // Create a proxy object that acts both as a Promise and an AsyncGenerator for backward compatibility
281
- //... @ts-ignore - We're deliberately implementing a proxy with dual behavior
282
- return createBackwardCompatStreamingProxy(streamPromise);
283
- }
284
-
285
- /**
286
- * Buffer streaming results into a single response for cases where
287
- * we need to use streaming internally but the caller requested non-streaming
288
- */
289
- async function bufferStreamingResults(prompt: string | Message[], options: CallAIOptions): Promise<string> {
290
- // Create a copy of options with streaming enabled
291
- const streamingOptions = {
292
- ...options,
293
- stream: true,
294
- };
295
-
296
- try {
297
- // Get streaming generator
298
- const generator = (await callAi(prompt, streamingOptions)) as AsyncGenerator<string, string, unknown>;
299
-
300
- // For Claude JSON responses, take only the last chunk (the final processed result)
301
- // For all other cases, concatenate chunks as before
302
- const isClaudeJson = /claude/.test(options.model || "") && options.schema;
303
-
304
- if (isClaudeJson) {
305
- // For Claude with JSON schema, we only want the last yielded value
306
- // which will be the complete, properly processed JSON
307
- let lastChunk = "";
308
- for await (const chunk of generator) {
309
- // Replace the last chunk entirely instead of concatenating
310
- lastChunk = chunk;
311
- }
312
- return lastChunk;
313
- } else {
314
- // For all other cases, concatenate chunks
315
- let result = "";
316
- for await (const chunk of generator) {
317
- result += chunk;
318
- }
319
- return result;
320
- }
321
- } catch (error) {
322
- // Handle errors with standard API error handling
323
- await handleApiError(error as CallAIErrorParams, "Buffered streaming", options.debug, {
324
- apiKey: options.apiKey,
325
- endpoint: options.endpoint,
326
- skipRefresh: options.skipRefresh,
327
- refreshToken: options.refreshToken,
328
- updateRefreshToken: options.updateRefreshToken,
329
- });
330
- // If we get here, key was refreshed successfully, retry the operation with the new key
331
- // Retry with the refreshed key
332
- return bufferStreamingResults(prompt, {
333
- ...options,
334
- apiKey: keyStore.current || undefined, // Use the refreshed key from keyStore
335
- });
336
- }
337
-
338
- // This line should never be reached, but it satisfies the linter by ensuring
339
- // all code paths return a value
340
- throw new Error("Unexpected code path in bufferStreamingResults");
341
- }
342
-
343
- /**
344
- * Standardized API error handler
345
- */
346
- // createBackwardCompatStreamingProxy is imported from api-core.ts
347
-
348
- // handleApiError is imported from error-handling.ts
349
-
350
- // checkForInvalidModelError is imported from error-handling.ts
351
-
352
- /**
353
- * Prepare request parameters common to both streaming and non-streaming calls
354
- */
355
- function prepareRequestParams(
356
- prompt: string | Message[],
357
- options: CallAIOptions,
358
- ): {
359
- apiKey: string;
360
- model: string;
361
- endpoint: string;
362
- requestOptions: RequestInit;
363
- schemaStrategy: SchemaStrategy;
364
- } {
365
- // First try to get the API key from options or window globals
366
- const apiKey = options.apiKey || keyStore.current || callAiEnv.CALLAI_API_KEY; // Try keyStore first in case it was refreshed in a previous call
367
- const schema = options.schema || null;
368
-
369
- // If no API key exists, we won't throw immediately. We'll continue and let handleApiError
370
- // attempt to fetch a key if needed. This will be handled later in the call chain.
371
-
372
- // Select the appropriate strategy based on model and schema
373
- const schemaStrategy = chooseSchemaStrategy(options.model, schema);
374
- const model = schemaStrategy.model;
375
-
376
- // Get custom chat API origin if set
377
- const customChatOrigin = options.chatUrl || callAiEnv.CALLAI_CHAT_URL;
378
-
379
- // Use custom origin or default OpenRouter URL
380
- const endpoint =
381
- options.endpoint ||
382
- (customChatOrigin ? `${customChatOrigin}/api/v1/chat/completions` : "https://openrouter.ai/api/v1/chat/completions");
383
-
384
- // Handle both string prompts and message arrays for backward compatibility
385
- const messages: Message[] = Array.isArray(prompt) ? prompt : [{ role: "user", content: prompt }];
386
-
387
- // Common parameters for both streaming and non-streaming
388
- const requestParams: CallAIOptions = {
389
- model,
390
- messages,
391
- stream: options.stream !== undefined ? options.stream : false,
392
- };
393
-
394
- // Only include temperature if explicitly set
395
- if (options.temperature) {
396
- requestParams.temperature = options.temperature;
397
- }
398
-
399
- // Only include top_p if explicitly set
400
- if (options.topP !== undefined) {
401
- requestParams.top_p = options.topP;
402
- }
403
-
404
- // Only include max_tokens if explicitly set
405
- if (options.maxTokens !== undefined) {
406
- requestParams.max_tokens = options.maxTokens;
407
- }
408
-
409
- // Add optional parameters if specified
410
- if (options.stop) {
411
- // Handle both single string and array of stop sequences
412
- requestParams.stop = Array.isArray(options.stop) ? options.stop : [options.stop];
413
- }
414
-
415
- // Add response_format parameter for models that support JSON output
416
- if (options.responseFormat === "json") {
417
- requestParams.response_format = { type: "json_object" };
418
- }
419
-
420
- // Add schema structure if provided (for function calling/JSON mode)
421
- if (schema) {
422
- // Apply schema-specific parameters using the selected strategy
423
- Object.assign(requestParams, schemaStrategy.prepareRequest(schema, messages));
424
- }
425
-
426
- // HTTP headers for the request
427
- const headers: Record<string, string> = {
428
- Authorization: `Bearer ${apiKey}`,
429
- "Content-Type": "application/json",
430
- "HTTP-Referer": options.referer || "https://vibes.diy",
431
- "X-Title": options.title || "Vibes",
432
- };
433
-
434
- // Add any additional headers
435
- if (options.headers) {
436
- Object.assign(headers, options.headers);
437
- }
438
-
439
- // Build the requestOptions object for fetch
440
- const requestOptions: RequestInit = {
441
- method: "POST",
442
- headers: {
443
- ...headers,
444
- "Content-Type": "application/json",
445
- },
446
- body: JSON.stringify(requestParams),
447
- };
448
-
449
- // If we don't have an API key, throw a clear error that can be caught and handled
450
- // by the error handling system to trigger key fetching
451
- if (!apiKey) {
452
- throw new Error("API key is required. Provide it via options.apiKey or set window.CALLAI_API_KEY");
453
- }
454
-
455
- // Debug logging for request payload
456
- if (options.debug) {
457
- console.log(`[callAi-prepareRequest:raw] Endpoint: ${endpoint}`);
458
- console.log(`[callAi-prepareRequest:raw] Model: ${model}`);
459
- console.log(`[callAi-prepareRequest:raw] Payload:`, JSON.stringify(requestParams));
460
- }
461
-
462
- return { apiKey, model, endpoint, requestOptions, schemaStrategy };
463
- }
464
-
465
- /**
466
- * Internal implementation for non-streaming API calls
467
- */
468
- async function callAINonStreaming(prompt: string | Message[], options: CallAIOptions = {}, isRetry = false): Promise<string> {
469
- try {
470
- // Start timing for metadata
471
- const startTime = Date.now();
472
-
473
- // Create metadata object
474
- const meta: ResponseMeta = {
475
- model: options.model || "unknown",
476
- timing: {
477
- startTime: startTime,
478
- },
479
- };
480
- const { endpoint, requestOptions, model, schemaStrategy } = prepareRequestParams(prompt, options);
481
-
482
- const response = await callAiFetch(options)(endpoint, requestOptions);
483
-
484
- // We don't store the raw Response object in metadata anymore
485
-
486
- // Handle HTTP errors, with potential fallback for invalid model
487
- if (!response.ok || response.status >= 400) {
488
- const { isInvalidModel } = await checkForInvalidModelError(response, model, options.debug);
489
-
490
- if (isInvalidModel) {
491
- // Retry with fallback model
492
- return callAINonStreaming(prompt, { ...options, model: FALLBACK_MODEL }, true);
493
- }
494
-
495
- // Create a proper error object with the status code preserved
496
- const error = new CallAIError({
497
- message: `HTTP error! Status: ${response.status}`,
498
- status: response.status,
499
- statusText: response.statusText,
500
- details: undefined,
501
- contentType: "text/plain",
502
- });
503
- throw error;
504
- }
505
-
506
- let result;
507
-
508
- // For Claude, use text() instead of json() to avoid potential hanging
509
- if (/claude/i.test(model)) {
510
- try {
511
- result = await extractClaudeResponse(response);
512
- } catch (error) {
513
- handleApiError(error as CallAIErrorParams, "Claude API response processing failed", options.debug);
514
- }
515
- } else {
516
- result = await response.json();
517
- }
518
-
519
- // Debug logging for raw API response
520
- if (options.debug) {
521
- console.log(`[callAi-nonStreaming:raw] Response:`, JSON.stringify(result));
522
- }
523
-
524
- // Handle error responses
525
- if (result.error) {
526
- if (options.debug) {
527
- console.error("API returned an error:", result.error);
528
- }
529
- // If it's a model error and not already a retry, try with fallback
530
- if (
531
- !isRetry &&
532
- !options.skipRetry &&
533
- result.error.message &&
534
- result.error.message.toLowerCase().includes("not a valid model")
535
- ) {
536
- if (options.debug) {
537
- console.warn(`Model ${model} error, retrying with ${FALLBACK_MODEL}`);
538
- }
539
- return callAINonStreaming(prompt, { ...options, model: FALLBACK_MODEL }, true);
540
- }
541
- return JSON.stringify({
542
- error: result.error,
543
- message: result.error.message || "API returned an error",
544
- });
545
- }
546
-
547
- // Extract content from the response
548
- const content = extractContent(result, schemaStrategy);
549
-
550
- // Store the raw response data for user access
551
- if (result) {
552
- // Store the parsed JSON result from the API call
553
- meta.rawResponse = result;
554
- }
555
-
556
- // Update model info
557
- meta.model = model;
558
-
559
- // Update timing info
560
- if (meta.timing) {
561
- meta.timing.endTime = Date.now();
562
- meta.timing.duration = meta.timing.endTime - meta.timing.startTime;
563
- }
564
-
565
- // Process the content based on model type
566
- const processedContent = schemaStrategy.processResponse(content);
567
-
568
- // Box the string for WeakMap storage
569
- const boxed = boxString(processedContent);
570
- responseMetadata.set(boxed, meta);
571
-
572
- return processedContent;
573
- } catch (error) {
574
- await handleApiError(error, "Non-streaming API call", options.debug, {
575
- apiKey: options.apiKey,
576
- endpoint: options.endpoint,
577
- skipRefresh: options.skipRefresh,
578
- refreshToken: options.refreshToken,
579
- updateRefreshToken: options.updateRefreshToken,
580
- });
581
- // If we get here, key was refreshed successfully, retry the operation with the new key
582
- // Retry with the refreshed key
583
- return callAINonStreaming(
584
- prompt,
585
- {
586
- ...options,
587
- apiKey: keyStore.current || undefined, // Use the refreshed key from keyStore
588
- },
589
- true,
590
- ); // Set isRetry to true
591
- }
592
-
593
- // This line will never be reached, but it satisfies the linter
594
- throw new Error("Unexpected code path in callAINonStreaming");
595
- }
package/image.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import { ImageGenOptions, ImageResponse } from "./types.js";
2
- export declare function imageGen(prompt: string, options?: ImageGenOptions): Promise<ImageResponse>;
package/image.js DELETED
@@ -1,80 +0,0 @@
1
- import { callAiFetch, joinUrlParts } from "./utils.js";
2
- import { callAiEnv } from "./env.js";
3
- export async function imageGen(prompt, options = {}) {
4
- const { model = "google/gemini-2.5-flash-image", apiKey = callAiEnv.CALLAI_API_KEY, size = "1024x1024" } = options;
5
- if (!apiKey) {
6
- throw new Error("API key is required for image generation. Provide via options.apiKey or set window.CALLAI_API_KEY");
7
- }
8
- const customOrigin = options.imgUrl || callAiEnv.CALLAI_IMG_URL;
9
- if (!options.images || options.images.length === 0) {
10
- const generateEndpoint = options.endpoint || joinUrlParts(customOrigin || callAiEnv.def.CALLAI_CHAT_URL, "/api/openrouter-image/generate");
11
- if (!apiKey) {
12
- throw new Error("API key is required for image generation (simple)");
13
- }
14
- const headers = new Headers({
15
- Authorization: `Bearer ${apiKey}`,
16
- "Content-Type": "application/json",
17
- });
18
- const response = await callAiFetch(options)(generateEndpoint, {
19
- method: "POST",
20
- headers,
21
- body: JSON.stringify({
22
- model,
23
- prompt,
24
- size,
25
- ...(options.quality && { quality: options.quality }),
26
- ...(options.style && { style: options.style }),
27
- }),
28
- });
29
- if (!response.ok) {
30
- const errorData = await response.text();
31
- throw new Error(`Image generation failed: ${response.status} ${response.statusText} - ${errorData}`);
32
- }
33
- const responseText = await response.text();
34
- try {
35
- const result = JSON.parse(responseText);
36
- return result;
37
- }
38
- catch (parseError) {
39
- throw new Error(`Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : "Unknown error"}`);
40
- }
41
- }
42
- else {
43
- const formData = new FormData();
44
- formData.append("model", model);
45
- formData.append("prompt", prompt);
46
- options.images.forEach((image, index) => {
47
- formData.append(`image_${index}`, image);
48
- });
49
- formData.append("size", size);
50
- if (options.quality)
51
- formData.append("quality", options.quality);
52
- if (options.style)
53
- formData.append("style", options.style);
54
- const editEndpoint = options.endpoint || joinUrlParts(customOrigin || callAiEnv.def.CALLAI_CHAT_URL, "/api/openrouter-image/edit");
55
- if (!apiKey) {
56
- throw new Error("API key is required for image generation (edit)");
57
- }
58
- const headers = new Headers({
59
- Authorization: `Bearer ${apiKey}`,
60
- });
61
- const response = await callAiFetch(options)(editEndpoint, {
62
- method: "POST",
63
- headers,
64
- body: formData,
65
- });
66
- if (!response.ok) {
67
- const errorData = await response.text();
68
- throw new Error(`Image editing failed: ${response.status} ${response.statusText} - ${errorData}`);
69
- }
70
- const responseText = await response.text();
71
- try {
72
- const result = JSON.parse(responseText);
73
- return result;
74
- }
75
- catch (parseError) {
76
- throw new Error(`Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : "Unknown error"}`);
77
- }
78
- }
79
- }
80
- //# sourceMappingURL=image.js.map
package/image.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"image.js","sourceRoot":"","sources":["../jsr/image.ts"],"names":[],"mappings":"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"}
package/index.ts.bak DELETED
@@ -1,16 +0,0 @@
1
- /**
2
- * call-ai: A lightweight library for making AI API calls
3
- */
4
-
5
- // Export public types
6
- export * from "./types";
7
-
8
- // Export API functions
9
- export { callAi, getMeta } from "./api";
10
-
11
- // Export image generation function
12
- export { imageGen } from "./image";
13
-
14
- // Export strategies and utilities for advanced use cases
15
- export * from "./strategies";
16
- export * from "./utils";
package/streaming.ts.off DELETED
@@ -1,571 +0,0 @@
1
- /**
2
- * Streaming response handling for call-ai
3
- */
4
-
5
- import { CallAIError, CallAIOptions, Message, ResponseMeta, SchemaAIMessageRequest, SchemaStrategy, ToolUseType } from "./types.js";
6
- import { globalDebug } from "./key-management.js";
7
- import { responseMetadata, boxString } from "./response-metadata.js";
8
- import { checkForInvalidModelError } from "./error-handling.js";
9
- import { PACKAGE_VERSION, FALLBACK_MODEL } from "./non-streaming.js";
10
- import { callAiFetch } from "./utils.js";
11
-
12
- // Generator factory function for streaming API calls
13
- // This is called after the fetch is made and response is validated
14
- //
15
- // Note: Even though we checked response.ok before creating this generator,
16
- // we need to be prepared for errors that may occur during streaming. Some APIs
17
- // return a 200 OK initially but then deliver error information in the stream.
18
- async function* createStreamingGenerator(
19
- response: Response,
20
- options: CallAIOptions,
21
- schemaStrategy: SchemaStrategy,
22
- model: string,
23
- ): AsyncGenerator<string, string, unknown> {
24
- // Create a metadata object for this streaming response
25
- const meta: ResponseMeta = {
26
- model,
27
- endpoint: options.endpoint || "https://openrouter.ai/api/v1",
28
- timing: {
29
- startTime: Date.now(),
30
- endTime: 0,
31
- duration: 0,
32
- },
33
- };
34
-
35
- // Tool calls assembly (for Claude/Anthropic)
36
- let toolCallsAssembled = "";
37
- let completeText = "";
38
- let chunkCount = 0;
39
-
40
- if (options.debug || globalDebug) {
41
- console.log(`[callAi:${PACKAGE_VERSION}] Starting streaming generator with model: ${model}`);
42
- }
43
-
44
- try {
45
- // Handle streaming response
46
- const reader = response.body?.getReader();
47
- if (!reader) {
48
- throw new Error("Response body is undefined - API endpoint may not support streaming");
49
- }
50
-
51
- const textDecoder = new TextDecoder();
52
- let buffer = ""; // Buffer to accumulate partial SSE messages
53
-
54
- while (true) {
55
- const { done, value } = await reader.read();
56
- if (done) {
57
- if (options.debug || globalDebug) {
58
- console.log(`[callAi-streaming:complete v${PACKAGE_VERSION}] Stream finished after ${chunkCount} chunks`);
59
- }
60
- break;
61
- }
62
-
63
- // Convert bytes to text
64
- const chunk = textDecoder.decode(value, { stream: true });
65
- buffer += chunk;
66
-
67
- // Split on double newlines to find complete SSE messages
68
- const messages = buffer.split(/\n\n/);
69
- buffer = messages.pop() || ""; // Keep the last incomplete chunk in the buffer
70
-
71
- for (const message of messages) {
72
- if (!message.trim() || !message.startsWith("data: ")) {
73
- continue; // Skip empty lines or non-data messages
74
- }
75
-
76
- // Extract the JSON payload
77
- const jsonStr = message.slice("data: ".length); // Remove 'data: ' prefix
78
- if (jsonStr === "[DONE]") {
79
- if (options.debug || globalDebug) {
80
- console.log(`[callAi:${PACKAGE_VERSION}] Received [DONE] signal`);
81
- }
82
- continue;
83
- }
84
-
85
- chunkCount++;
86
-
87
- // Try to parse the JSON
88
- try {
89
- console.log(`[callAi:${PACKAGE_VERSION}] Raw chunk #${chunkCount}:`, jsonStr);
90
- const json = JSON.parse(jsonStr);
91
-
92
- // Check for error responses in the stream
93
- if (
94
- json.error ||
95
- json.type === "error" ||
96
- (json.choices && json.choices.length > 0 && json.choices[0].finish_reason === "error")
97
- ) {
98
- // Extract error message
99
- const errorMessage =
100
- json.error?.message || json.error || json.choices?.[0]?.message?.content || "Unknown streaming error";
101
-
102
- if (options.debug || globalDebug) {
103
- console.error(`[callAi:${PACKAGE_VERSION}] Detected error in streaming response:`, json);
104
- }
105
-
106
- // Create a detailed error to throw
107
- const detailedError = new CallAIError({
108
- message: `API streaming error: ${errorMessage}`,
109
- status: json.error?.status || 400,
110
- statusText: json.error?.type || "Bad Request",
111
- details: JSON.stringify(json.error || json),
112
- contentType: "application/json",
113
- });
114
- console.error(`[callAi:${PACKAGE_VERSION}] Throwing stream error:`, detailedError);
115
- throw detailedError;
116
- }
117
-
118
- // Handle tool use response - Claude with schema cases
119
- const isClaudeWithSchema = /claude/i.test(model) && schemaStrategy.strategy === "tool_mode";
120
-
121
- if (isClaudeWithSchema) {
122
- // Claude streaming tool calls - need to assemble arguments
123
- if (json.choices && json.choices.length > 0) {
124
- const choice = json.choices[0];
125
-
126
- // Handle finish reason tool_calls - this is where we know the tool call is complete
127
- if (choice.finish_reason === "tool_calls") {
128
- if (options.debug) {
129
- console.log(`[callAi:${PACKAGE_VERSION}] Received tool_calls finish reason. Assembled JSON:`, toolCallsAssembled);
130
- }
131
-
132
- // Full JSON collected, construct a proper object with it
133
- try {
134
- // Try to fix any malformed JSON that might have resulted from chunking
135
- // This happens when property names get split across chunks
136
- if (toolCallsAssembled) {
137
- try {
138
- // First try parsing as-is
139
- JSON.parse(toolCallsAssembled);
140
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
141
- } catch (e) {
142
- if (options.debug) {
143
- console.log(
144
- `[callAi:${PACKAGE_VERSION}] Attempting to fix malformed JSON in tool call:`,
145
- toolCallsAssembled,
146
- );
147
- }
148
-
149
- // Apply comprehensive fixes for Claude's JSON property splitting
150
- let fixedJson = toolCallsAssembled;
151
-
152
- // 1. Remove trailing commas
153
- fixedJson = fixedJson.replace(/,\s*([}\]])/, "$1");
154
-
155
- // 2. Ensure proper JSON structure
156
- // Add closing braces if missing
157
- const openBraces = (fixedJson.match(/\{/g) || []).length;
158
- const closeBraces = (fixedJson.match(/\}/g) || []).length;
159
- if (openBraces > closeBraces) {
160
- fixedJson += "}".repeat(openBraces - closeBraces);
161
- }
162
-
163
- // Add opening brace if missing
164
- if (!fixedJson.trim().startsWith("{")) {
165
- fixedJson = "{" + fixedJson.trim();
166
- }
167
-
168
- // Ensure it ends with a closing brace
169
- if (!fixedJson.trim().endsWith("}")) {
170
- fixedJson += "}";
171
- }
172
-
173
- // 3. Fix various property name/value split issues
174
- // Fix dangling property names without values
175
- fixedJson = fixedJson.replace(/"(\w+)"\s*:\s*$/g, '"$1":null');
176
-
177
- // Fix missing property values
178
- fixedJson = fixedJson.replace(/"(\w+)"\s*:\s*,/g, '"$1":null,');
179
-
180
- // Fix incomplete property names (when split across chunks)
181
- fixedJson = fixedJson.replace(/"(\w+)"\s*:\s*"(\w+)$/g, '"$1$2"');
182
-
183
- // Balance brackets
184
- const openBrackets = (fixedJson.match(/\[/g) || []).length;
185
- const closeBrackets = (fixedJson.match(/\]/g) || []).length;
186
- if (openBrackets > closeBrackets) {
187
- fixedJson += "]".repeat(openBrackets - closeBrackets);
188
- }
189
-
190
- if (options.debug) {
191
- console.log(
192
- `[callAi:${PACKAGE_VERSION}] Applied comprehensive JSON fixes:`,
193
- `\nBefore: ${toolCallsAssembled}`,
194
- `\nAfter: ${fixedJson}`,
195
- );
196
- }
197
-
198
- toolCallsAssembled = fixedJson;
199
- }
200
- }
201
-
202
- // Return the assembled tool call
203
- completeText = toolCallsAssembled;
204
- yield completeText;
205
- continue;
206
- } catch (e) {
207
- console.error("[callAIStreaming] Error handling assembled tool call:", e);
208
- }
209
- }
210
-
211
- // Assemble tool_calls arguments from delta
212
- // Simply accumulate the raw strings without trying to parse them
213
- if (choice && choice.delta && choice.delta.tool_calls) {
214
- const toolCall = choice.delta.tool_calls[0];
215
- if (toolCall && toolCall.function && toolCall.function.arguments !== undefined) {
216
- toolCallsAssembled += toolCall.function.arguments;
217
- if (options.debug) {
218
- console.log(`[callAi:${PACKAGE_VERSION}] Accumulated tool call chunk:`, toolCall.function.arguments);
219
- }
220
- }
221
- }
222
- }
223
- }
224
-
225
- // Handle tool use response - old format
226
- if (isClaudeWithSchema && (json.stop_reason === "tool_use" || json.type === "tool_use")) {
227
- // First try direct tool use object format
228
- if (json.type === "tool_use") {
229
- completeText = schemaStrategy.processResponse(json);
230
- yield completeText;
231
- continue;
232
- }
233
-
234
- // Extract the tool use content
235
- if (json.content && Array.isArray(json.content)) {
236
- const toolUseBlock = json.content.find((block: ToolUseType) => block.type === "tool_use");
237
- if (toolUseBlock) {
238
- completeText = schemaStrategy.processResponse(toolUseBlock);
239
- yield completeText;
240
- continue;
241
- }
242
- }
243
-
244
- // Find tool_use in assistant's content blocks
245
- if (json.choices && Array.isArray(json.choices)) {
246
- const choice = json.choices[0];
247
- if (choice.message && Array.isArray(choice.message.content)) {
248
- const toolUseBlock = choice.message.content.find((block: ToolUseType) => block.type === "tool_use");
249
- if (toolUseBlock) {
250
- completeText = schemaStrategy.processResponse(toolUseBlock);
251
- yield completeText;
252
- continue;
253
- }
254
- }
255
-
256
- // Handle case where the tool use is in the delta
257
- if (choice.delta && Array.isArray(choice.delta.content)) {
258
- const toolUseBlock = choice.delta.content.find((block: ToolUseType) => block.type === "tool_use");
259
- if (toolUseBlock) {
260
- completeText = schemaStrategy.processResponse(toolUseBlock);
261
- yield completeText;
262
- continue;
263
- }
264
- }
265
- }
266
- }
267
-
268
- // Extract content from the delta
269
- if (json.choices?.[0]?.delta?.content !== undefined) {
270
- const content = json.choices[0].delta.content || "";
271
-
272
- // Treat all models the same - yield as content arrives
273
- completeText += content;
274
- yield schemaStrategy.processResponse(completeText);
275
- }
276
- // Handle message content format (non-streaming deltas)
277
- else if (json.choices?.[0]?.message?.content !== undefined) {
278
- const content = json.choices[0].message.content || "";
279
- completeText += content;
280
- yield schemaStrategy.processResponse(completeText);
281
- }
282
- // Handle content blocks for Claude/Anthropic response format
283
- else if (json.choices?.[0]?.message?.content && Array.isArray(json.choices[0].message.content)) {
284
- const contentBlocks = json.choices[0].message.content;
285
- // Find text or tool_use blocks
286
- for (const block of contentBlocks) {
287
- if (block.type === "text") {
288
- completeText += block.text || "";
289
- } else if (isClaudeWithSchema && block.type === "tool_use") {
290
- completeText = schemaStrategy.processResponse(block);
291
- break; // We found what we need
292
- }
293
- }
294
-
295
- yield schemaStrategy.processResponse(completeText);
296
- }
297
-
298
- // Find text delta for content blocks (Claude format)
299
- if (json.type === "content_block_delta" && json.delta && json.delta.type === "text_delta" && json.delta.text) {
300
- if (options.debug) {
301
- console.log(`[callAi:${PACKAGE_VERSION}] Received text delta:`, json.delta.text);
302
- }
303
- completeText += json.delta.text;
304
- // In some models like Claude, don't yield partial results as they can be malformed JSON
305
- // Only yield what we've seen so far if it's not a Claude model with schema
306
- if (!isClaudeWithSchema) {
307
- yield schemaStrategy.processResponse(completeText);
308
- }
309
- }
310
- } catch (e) {
311
- if (options.debug) {
312
- console.error(`[callAIStreaming] Error parsing JSON chunk:`, e);
313
- }
314
- }
315
- }
316
- }
317
-
318
- // We no longer need special error handling here as errors are thrown immediately
319
-
320
- // No extra error handling needed here - errors are thrown immediately
321
-
322
- // If we have assembled tool calls but haven't yielded them yet
323
- if (toolCallsAssembled && (!completeText || completeText.length === 0)) {
324
- // Try to fix any remaining JSON issues before returning
325
- let result = toolCallsAssembled;
326
-
327
- try {
328
- // Try to parse as-is first
329
- JSON.parse(result);
330
- } catch (e) {
331
- if (options.debug) {
332
- console.log(`[callAi:${PACKAGE_VERSION}] Final JSON validation failed:`, e, `\nAttempting to fix JSON:`, result);
333
- }
334
-
335
- // Apply more robust fixes for Claude's streaming JSON issues
336
-
337
- // 1. Remove trailing commas (common in malformed JSON)
338
- result = result.replace(/,\s*([}\]])/, "$1");
339
-
340
- // 2. Ensure we have proper JSON structure
341
- // Add closing braces if missing
342
- const openBraces = (result.match(/\{/g) || []).length;
343
- const closeBraces = (result.match(/\}/g) || []).length;
344
- if (openBraces > closeBraces) {
345
- result += "}".repeat(openBraces - closeBraces);
346
- }
347
-
348
- // Add opening brace if missing
349
- if (!result.trim().startsWith("{")) {
350
- result = "{" + result.trim();
351
- }
352
-
353
- // Ensure it ends with a closing brace
354
- if (!result.trim().endsWith("}")) {
355
- result += "}";
356
- }
357
-
358
- // Fix dangling property names without values
359
- result = result.replace(/"(\w+)"\s*:\s*$/g, '"$1":null');
360
-
361
- // Fix missing property values
362
- result = result.replace(/"(\w+)"\s*:\s*,/g, '"$1":null,');
363
-
364
- // Balance brackets
365
- const openBrackets = (result.match(/\[/g) || []).length;
366
- const closeBrackets = (result.match(/\]/g) || []).length;
367
- if (openBrackets > closeBrackets) {
368
- result += "]".repeat(openBrackets - closeBrackets);
369
- }
370
-
371
- if (options.debug) {
372
- console.log(`[callAi:${PACKAGE_VERSION}] Applied final JSON fixes:`, result);
373
- }
374
- }
375
-
376
- // Return the assembled tool call
377
- completeText = result;
378
-
379
- // Try one more time to validate
380
- try {
381
- JSON.parse(completeText);
382
- } catch (finalParseError) {
383
- if (options.debug) {
384
- console.error(`[callAi:${PACKAGE_VERSION}] Final JSON validation still failed:`, finalParseError);
385
- }
386
- }
387
-
388
- yield completeText;
389
- }
390
-
391
- // Record streaming completion in metadata
392
- const endTime = Date.now();
393
- meta.timing.endTime = endTime;
394
- meta.timing.duration = endTime - meta.timing.startTime;
395
-
396
- // Add the rawResponse field to match non-streaming behavior
397
- // For streaming, we use the final complete text as the raw response
398
- meta.rawResponse = completeText;
399
-
400
- // Store metadata for this response
401
- const boxed = boxString(completeText);
402
- responseMetadata.set(boxed, meta);
403
-
404
- // Return the complete text as the final value
405
- return completeText;
406
- } catch (error) {
407
- // Streaming generators must properly handle errors
408
- if (options.debug || globalDebug) {
409
- console.error(`[callAi:${PACKAGE_VERSION}] Streaming error:`, error);
410
- }
411
-
412
- // This error will be caught in the caller's try/catch block
413
- throw error;
414
- }
415
- }
416
-
417
- // Simplified generator for accessing streaming results
418
- // Returns an async generator that yields blocks of text
419
- // This is a higher-level function that prepares the request
420
- // and handles model fallback
421
- async function* callAIStreaming(
422
- prompt: string | Message[],
423
- options: CallAIOptions = {},
424
- isRetry = false,
425
- ): AsyncGenerator<string, string, unknown> {
426
- // Convert simple string prompts to message array format
427
- const messages = Array.isArray(prompt) ? prompt : [{ role: "user", content: prompt } satisfies Message];
428
-
429
- // API key should be provided by options (validation happens in callAi)
430
- const apiKey = options.apiKey;
431
- const model = options.model || "openai/gpt-3.5-turbo";
432
-
433
- // Default endpoint compatible with OpenAI API
434
- const endpoint = options.endpoint || "https://openrouter.ai/api/v1";
435
-
436
- // Build the endpoint URL
437
- const url = `${endpoint}/chat/completions`;
438
-
439
- // Choose a schema strategy based on model
440
- const schemaStrategy = options.schemaStrategy;
441
- if (!schemaStrategy) {
442
- throw new Error("Schema strategy is required for streaming");
443
- }
444
-
445
- // Default to JSON response for certain models
446
- const responseFormat = options.responseFormat || /gpt-4/.test(model) || /gpt-3.5/.test(model) ? "json" : undefined;
447
-
448
- const debug = options.debug === undefined ? globalDebug : options.debug;
449
-
450
- if (debug) {
451
- console.log(`[callAi:${PACKAGE_VERSION}] Making streaming request to: ${url}`);
452
- console.log(`[callAi:${PACKAGE_VERSION}] With model: ${model}`);
453
- }
454
-
455
- // Build request body
456
- const requestBody: SchemaAIMessageRequest = {
457
- model,
458
- messages,
459
- max_tokens: options.maxTokens || 2048,
460
- temperature: options.temperature !== undefined ? options.temperature : 0.7,
461
- top_p: options.topP ? options.topP : 1,
462
- stream: true,
463
- };
464
-
465
- // Add response_format if specified or for JSON handling
466
- if (responseFormat === "json") {
467
- requestBody.response_format = { type: "json_object" };
468
- }
469
-
470
- // Add schema-specific parameters (if schema is provided)
471
- if (options.schema) {
472
- Object.assign(requestBody, schemaStrategy?.prepareRequest(options.schema, messages));
473
- }
474
-
475
- // Add HTTP referer and other options to help with abuse prevention
476
- const headers: Record<string, string> = {
477
- Authorization: `Bearer ${apiKey}`,
478
- "HTTP-Referer": options.referer || "https://vibes.diy",
479
- "X-Title": options.title || "Vibes",
480
- "Content-Type": "application/json",
481
- };
482
-
483
- // Add any additional headers
484
- if (options.headers) {
485
- Object.assign(headers, options.headers);
486
- }
487
-
488
- // Copy any other options not explicitly handled above
489
- Object.keys(options).forEach((key) => {
490
- if (
491
- ![
492
- "apiKey",
493
- "model",
494
- "endpoint",
495
- "stream",
496
- "schema",
497
- "maxTokens",
498
- "temperature",
499
- "topP",
500
- "responseFormat",
501
- "referer",
502
- "title",
503
- "headers",
504
- "skipRefresh",
505
- "debug",
506
- ].includes(key)
507
- ) {
508
- requestBody[key] = options[key];
509
- }
510
- });
511
-
512
- if (debug) {
513
- console.log(`[callAi:${PACKAGE_VERSION}] Request headers:`, headers);
514
- console.log(`[callAi:${PACKAGE_VERSION}] Request body:`, requestBody);
515
- }
516
-
517
- let response;
518
- try {
519
- // Make the API request
520
- response = await callAiFetch(options)(url, {
521
- method: "POST",
522
- headers,
523
- body: JSON.stringify(requestBody),
524
- });
525
-
526
- // Handle HTTP errors
527
- if (!response.ok) {
528
- // Check if this is an invalid model error that we can handle with a fallback
529
- const { isInvalidModel, errorData } = await checkForInvalidModelError(response, model, debug);
530
-
531
- if (isInvalidModel && !isRetry && !options.skipRetry) {
532
- if (debug) {
533
- console.log(`[callAi:${PACKAGE_VERSION}] Invalid model "${model}", falling back to "${FALLBACK_MODEL}"`);
534
- }
535
-
536
- // Retry with the fallback model using yield* to delegate to the other generator
537
- yield* callAIStreaming(
538
- prompt,
539
- {
540
- ...options,
541
- model: FALLBACK_MODEL,
542
- },
543
- true, // Mark as retry to prevent infinite fallback loops
544
- );
545
-
546
- // Generator delegation handles returning the final value
547
- return "";
548
- }
549
-
550
- // For other errors, throw with details
551
- const errorText = errorData ? JSON.stringify(errorData) : `HTTP error! Status: ${response.status}`;
552
- throw new Error(errorText);
553
- }
554
-
555
- // Yield streaming results through the generator
556
- yield* createStreamingGenerator(response, options, schemaStrategy, model);
557
-
558
- // The createStreamingGenerator will return the final assembled string
559
- return ""; // This is never reached due to yield*
560
- } catch (fetchError) {
561
- // Network errors must be directly re-thrown without modification
562
- // This is exactly how the original implementation handles it
563
- if (debug) {
564
- console.error(`[callAi:${PACKAGE_VERSION}] Network error during fetch:`, fetchError);
565
- }
566
- // Critical: throw the exact same error object without any wrapping
567
- throw fetchError;
568
- }
569
- }
570
-
571
- export { createStreamingGenerator, callAIStreaming };