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 +0 -1
- package/index.js +0 -1
- package/index.js.map +1 -1
- package/package.json +1 -1
- package/types.d.ts +2 -2
- package/api.ts.off +0 -595
- package/image.d.ts +0 -2
- package/image.js +0 -80
- package/image.js.map +0 -1
- package/index.ts.bak +0 -16
- package/streaming.ts.off +0 -571
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;
|
|
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
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
|
|
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 =
|
|
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
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 };
|