call-ai 0.7.0-dev-preview-5 → 0.7.0-dev-preview-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/dist/api.d.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Core API implementation for call-ai
3
3
  */
4
- import { CallAIOptions, Message } from "./types";
4
+ import { CallAIOptions, Message, StreamResponse } from "./types";
5
5
  /**
6
6
  * Make an AI API call with the given options
7
7
  * @param prompt User prompt as string or an array of message objects
8
8
  * @param options Configuration options including optional schema for structured output
9
9
  * @returns A Promise that resolves to the complete response string when streaming is disabled,
10
- * or an AsyncGenerator that yields partial responses when streaming is enabled
10
+ * or a Promise that resolves to an AsyncGenerator when streaming is enabled.
11
+ * The AsyncGenerator yields partial responses as they arrive.
11
12
  */
12
- export declare function callAI(prompt: string | Message[], options?: CallAIOptions): Promise<string> | AsyncGenerator<string, string | undefined, unknown>;
13
+ export declare function callAI(prompt: string | Message[], options?: CallAIOptions): Promise<string | StreamResponse>;
package/dist/api.js CHANGED
@@ -12,7 +12,8 @@ const FALLBACK_MODEL = "openrouter/auto";
12
12
  * @param prompt User prompt as string or an array of message objects
13
13
  * @param options Configuration options including optional schema for structured output
14
14
  * @returns A Promise that resolves to the complete response string when streaming is disabled,
15
- * or an AsyncGenerator that yields partial responses when streaming is enabled
15
+ * or a Promise that resolves to an AsyncGenerator when streaming is enabled.
16
+ * The AsyncGenerator yields partial responses as they arrive.
16
17
  */
17
18
  function callAI(prompt, options = {}) {
18
19
  // Check if we need to force streaming based on model strategy
@@ -26,8 +27,59 @@ function callAI(prompt, options = {}) {
26
27
  if (options.stream !== true) {
27
28
  return callAINonStreaming(prompt, options);
28
29
  }
29
- // Handle streaming mode
30
- return callAIStreaming(prompt, options);
30
+ // Handle streaming mode - return a Promise that resolves to an AsyncGenerator
31
+ // but also supports legacy non-awaited usage for backward compatibility
32
+ const streamPromise = (async () => {
33
+ // Do setup and validation before returning the generator
34
+ const { endpoint, requestOptions, model, schemaStrategy } = prepareRequestParams(prompt, { ...options, stream: true });
35
+ // Make the fetch request and handle errors before creating the generator
36
+ const response = await fetch(endpoint, requestOptions);
37
+ // Enhanced error handling with more debugging
38
+ if (!response.ok) {
39
+ if (options.debug) {
40
+ console.error(`[callAI:${PACKAGE_VERSION}] HTTP Error:`, response.status, response.statusText, response.url);
41
+ }
42
+ // Check if this is an invalid model error
43
+ const { isInvalidModel, errorData } = await checkForInvalidModelError(response.clone(), // Clone response since we'll need to read body twice
44
+ model, false, options.skipRetry);
45
+ if (isInvalidModel && !options.skipRetry) {
46
+ if (options.debug) {
47
+ console.log(`[callAI:${PACKAGE_VERSION}] Retrying with fallback model: ${FALLBACK_MODEL}`);
48
+ }
49
+ // Retry with fallback model - it will return a promise
50
+ const result = await callAI(prompt, { ...options, model: FALLBACK_MODEL });
51
+ return result;
52
+ }
53
+ // Get full error text from body
54
+ const errorText = await response.text();
55
+ if (options.debug) {
56
+ console.error(`[callAI:${PACKAGE_VERSION}] Error response body:`, errorText);
57
+ }
58
+ // Create a detailed error with status information
59
+ const errorMessage = `API returned error ${response.status}: ${response.statusText}`;
60
+ const error = new Error(errorMessage);
61
+ // Add extra properties for more context
62
+ error.status = response.status;
63
+ error.statusText = response.statusText;
64
+ error.details = errorText;
65
+ // Ensure this error is thrown and caught properly in the Promise chain
66
+ if (options.debug) {
67
+ console.error(`[callAI:${PACKAGE_VERSION}] Throwing error:`, error);
68
+ }
69
+ throw error;
70
+ }
71
+ // Only if response is OK, create and return the streaming generator
72
+ return createStreamingGenerator(response, options, schemaStrategy, model);
73
+ })();
74
+ // For backward compatibility with v0.6.x where users didn't await the result
75
+ if (process.env.NODE_ENV !== 'production') {
76
+ console.warn(`[callAI:${PACKAGE_VERSION}] WARNING: Using callAI with streaming without await is deprecated. ` +
77
+ `Please use 'const generator = await callAI(...)' instead of 'const generator = callAI(...)'. ` +
78
+ `This backward compatibility will be removed in a future version.`);
79
+ }
80
+ // Create a proxy object that acts both as a Promise and an AsyncGenerator for backward compatibility
81
+ // @ts-ignore - We're deliberately implementing a proxy with dual behavior
82
+ return createBackwardCompatStreamingProxy(streamPromise);
31
83
  }
32
84
  /**
33
85
  * Buffer streaming results into a single response for cases where
@@ -41,7 +93,7 @@ async function bufferStreamingResults(prompt, options) {
41
93
  };
42
94
  try {
43
95
  // Get streaming generator
44
- const generator = callAIStreaming(prompt, streamingOptions);
96
+ const generator = await callAI(prompt, streamingOptions);
45
97
  // Buffer all chunks
46
98
  let finalResult = "";
47
99
  let chunkCount = 0;
@@ -55,6 +107,51 @@ async function bufferStreamingResults(prompt, options) {
55
107
  handleApiError(error, "Streaming buffer error", options.debug);
56
108
  }
57
109
  }
110
+ /**
111
+ * Standardized API error handler
112
+ */
113
+ /**
114
+ * Create a proxy that acts both as a Promise and an AsyncGenerator for backward compatibility
115
+ * @internal This is for internal use only, not part of public API
116
+ */
117
+ function createBackwardCompatStreamingProxy(promise) {
118
+ // Create a proxy that forwards methods to the Promise or AsyncGenerator as appropriate
119
+ return new Proxy({}, {
120
+ get(target, prop) {
121
+ // First check if it's an AsyncGenerator method (needed for for-await)
122
+ if (prop === 'next' || prop === 'throw' || prop === 'return' || prop === Symbol.asyncIterator) {
123
+ // Create wrapper functions that await the Promise first
124
+ if (prop === Symbol.asyncIterator) {
125
+ return function () {
126
+ return {
127
+ // Implement async iterator that gets the generator first
128
+ async next(value) {
129
+ try {
130
+ const generator = await promise;
131
+ return generator.next(value);
132
+ }
133
+ catch (error) {
134
+ // Turn Promise rejection into iterator result with error thrown
135
+ return Promise.reject(error);
136
+ }
137
+ }
138
+ };
139
+ };
140
+ }
141
+ // Methods like next, throw, return
142
+ return async function (value) {
143
+ const generator = await promise;
144
+ return generator[prop](value);
145
+ };
146
+ }
147
+ // Then check if it's a Promise method
148
+ if (prop === 'then' || prop === 'catch' || prop === 'finally') {
149
+ return promise[prop].bind(promise);
150
+ }
151
+ return undefined;
152
+ }
153
+ });
154
+ }
58
155
  /**
59
156
  * Standardized API error handler
60
157
  */
@@ -68,27 +165,74 @@ function handleApiError(error, context, debug = false) {
68
165
  * Helper to check if an error indicates invalid model and handle fallback
69
166
  */
70
167
  async function checkForInvalidModelError(response, model, isRetry, skipRetry = false) {
71
- // Skip retry immediately if skipRetry is true
72
- if (skipRetry || response.status !== 400 || isRetry) {
168
+ // Skip retry immediately if skipRetry is true or if we're already retrying
169
+ if (skipRetry || isRetry) {
170
+ return { isInvalidModel: false };
171
+ }
172
+ // We want to check all 4xx errors, not just 400
173
+ if (response.status < 400 || response.status >= 500) {
73
174
  return { isInvalidModel: false };
74
175
  }
75
176
  // Clone the response so we can read the body
76
177
  const clonedResponse = response.clone();
77
178
  try {
78
179
  const errorData = await clonedResponse.json();
79
- // Check if the error message indicates an invalid model
80
- if (errorData.error &&
81
- errorData.error.message &&
82
- errorData.error.message.toLowerCase().includes("not a valid model")) {
83
- console.warn(`Model ${model} not valid, retrying with ${FALLBACK_MODEL}`);
84
- return { isInvalidModel: true };
180
+ const debugEnabled = true; // Always log for now to help diagnose the issue
181
+ if (debugEnabled) {
182
+ console.log(`[callAI:${PACKAGE_VERSION}] Checking for invalid model error:`, {
183
+ model,
184
+ statusCode: response.status,
185
+ errorData
186
+ });
187
+ }
188
+ // Common patterns for invalid model errors across different providers
189
+ const invalidModelPatterns = [
190
+ "not a valid model",
191
+ "model .* does not exist",
192
+ "invalid model",
193
+ "unknown model",
194
+ "no provider was found",
195
+ "fake-model", // For our test case
196
+ "does-not-exist" // For our test case
197
+ ];
198
+ // Check if error message contains any of our patterns
199
+ let errorMessage = '';
200
+ if (errorData.error && errorData.error.message) {
201
+ errorMessage = errorData.error.message.toLowerCase();
202
+ }
203
+ else if (errorData.message) {
204
+ errorMessage = errorData.message.toLowerCase();
205
+ }
206
+ else {
207
+ errorMessage = JSON.stringify(errorData).toLowerCase();
208
+ }
209
+ // Test the error message against each pattern
210
+ const isInvalidModel = invalidModelPatterns.some(pattern => errorMessage.includes(pattern.toLowerCase()));
211
+ if (isInvalidModel && debugEnabled) {
212
+ console.warn(`[callAI:${PACKAGE_VERSION}] Model ${model} not valid, will retry with ${FALLBACK_MODEL}`);
85
213
  }
86
- return { isInvalidModel: false, errorData };
214
+ return { isInvalidModel, errorData };
87
215
  }
88
216
  catch (parseError) {
89
- // If we can't parse the response as JSON, continue with original error
90
- console.error("Failed to parse error response:", parseError);
91
- return { isInvalidModel: false };
217
+ // If we can't parse the response as JSON, try to read it as text
218
+ console.error("Failed to parse error response as JSON:", parseError);
219
+ try {
220
+ const textResponse = await response.clone().text();
221
+ console.log("Error response as text:", textResponse);
222
+ // Even if it's not JSON, check if it contains any of our known patterns
223
+ const lowerText = textResponse.toLowerCase();
224
+ const isInvalidModel = lowerText.includes("invalid model") ||
225
+ lowerText.includes("not exist") ||
226
+ lowerText.includes("fake-model");
227
+ if (isInvalidModel) {
228
+ console.warn(`[callAI:${PACKAGE_VERSION}] Detected invalid model in text response for ${model}`);
229
+ }
230
+ return { isInvalidModel, errorData: { text: textResponse } };
231
+ }
232
+ catch (textError) {
233
+ console.error("Failed to read error response as text:", textError);
234
+ return { isInvalidModel: false };
235
+ }
92
236
  }
93
237
  }
94
238
  /**
@@ -278,30 +422,11 @@ async function extractClaudeResponse(response) {
278
422
  }
279
423
  }
280
424
  /**
281
- * Internal implementation for streaming API calls
425
+ * Generator factory function for streaming API calls
426
+ * This is called after the fetch is made and response is validated
282
427
  */
283
- async function* callAIStreaming(prompt, options = {}, isRetry = false) {
428
+ async function* createStreamingGenerator(response, options, schemaStrategy, model) {
284
429
  try {
285
- const { endpoint, requestOptions, model, schemaStrategy } = prepareRequestParams(prompt, { ...options, stream: true });
286
- const response = await fetch(endpoint, requestOptions);
287
- if (!response.ok) {
288
- const { isInvalidModel } = await checkForInvalidModelError(response, model, isRetry, options.skipRetry);
289
- if (isInvalidModel) {
290
- // Retry with fallback model
291
- return yield* callAIStreaming(prompt, { ...options, model: FALLBACK_MODEL }, true);
292
- }
293
- const errorText = await response.text();
294
- console.error(`API Error: ${response.status} ${response.statusText}`, errorText);
295
- // Create a detailed error with status information
296
- const errorMessage = `API returned error ${response.status}: ${response.statusText}`;
297
- const error = new Error(errorMessage);
298
- // Add extra properties for more context
299
- error.status = response.status;
300
- error.statusText = response.statusText;
301
- error.details = errorText;
302
- // Throw immediately - we want this to propagate correctly in all environments
303
- throw error;
304
- }
305
430
  // Handle streaming response
306
431
  if (!response.body) {
307
432
  throw new Error("Response body is undefined - API endpoint may not support streaming");
package/dist/types.d.ts CHANGED
@@ -64,6 +64,16 @@ export interface SchemaStrategy {
64
64
  processResponse: ModelStrategy["processResponse"];
65
65
  shouldForceStream: boolean;
66
66
  }
67
+ /**
68
+ * Return type for streaming API calls
69
+ */
70
+ export type StreamResponse = AsyncGenerator<string, string, unknown>;
71
+ /**
72
+ * @internal
73
+ * Internal type for backward compatibility with v0.6.x
74
+ * This type is not exposed in public API documentation
75
+ */
76
+ export type ThenableStreamResponse = AsyncGenerator<string, string, unknown> & Promise<StreamResponse>;
67
77
  export interface CallAIOptions {
68
78
  /**
69
79
  * API key for authentication
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "call-ai",
3
- "version": "0.7.0-dev-preview-5",
3
+ "version": "0.7.0-dev-preview-7",
4
4
  "description": "Lightweight library for making AI API calls with streaming support",
5
5
  "main": "dist/index.js",
6
6
  "browser": "dist/index.js",