call-ai 0.8.3 → 0.8.5-dev-preview
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-core.d.ts +40 -0
- package/dist/api-core.js +313 -0
- package/dist/api.d.ts +2 -0
- package/dist/api.js +153 -479
- package/dist/error-handling.d.ts +12 -0
- package/dist/error-handling.js +176 -0
- package/dist/image.js +9 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/key-management.d.ts +43 -0
- package/dist/key-management.js +312 -0
- package/dist/non-streaming.d.ts +10 -0
- package/dist/non-streaming.js +265 -0
- package/dist/response-metadata.d.ts +18 -0
- package/dist/response-metadata.js +44 -0
- package/dist/strategies/model-strategies.js +2 -2
- package/dist/streaming.d.ts +7 -0
- package/dist/streaming.js +483 -0
- package/dist/types.d.ts +45 -0
- package/package.json +12 -13
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare function handleApiError(error: any, context: string, debug?: boolean, options?: {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
endpoint?: string;
|
|
4
|
+
skipRefresh?: boolean;
|
|
5
|
+
refreshToken?: string;
|
|
6
|
+
updateRefreshToken?: (currentToken: string) => Promise<string>;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
declare function checkForInvalidModelError(response: Response, model: string, debug?: boolean): Promise<{
|
|
9
|
+
isInvalidModel: boolean;
|
|
10
|
+
errorData?: any;
|
|
11
|
+
}>;
|
|
12
|
+
export { handleApiError, checkForInvalidModelError };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleApiError = handleApiError;
|
|
4
|
+
exports.checkForInvalidModelError = checkForInvalidModelError;
|
|
5
|
+
/**
|
|
6
|
+
* Error handling utilities for call-ai
|
|
7
|
+
*/
|
|
8
|
+
const key_management_1 = require("./key-management");
|
|
9
|
+
// Standardized API error handler
|
|
10
|
+
// @param error The error object
|
|
11
|
+
// @param context Context description for error messages
|
|
12
|
+
// @param debug Whether to log debug information
|
|
13
|
+
// @param options Options for error handling including key refresh control
|
|
14
|
+
async function handleApiError(error, context, debug = key_management_1.globalDebug, options = {}) {
|
|
15
|
+
// Extract error details
|
|
16
|
+
const errorMessage = error?.message || String(error);
|
|
17
|
+
const status = error?.status ||
|
|
18
|
+
error?.statusCode ||
|
|
19
|
+
error?.response?.status ||
|
|
20
|
+
(errorMessage.match(/status: (\d+)/i)?.[1] &&
|
|
21
|
+
parseInt(errorMessage.match(/status: (\d+)/i)[1]));
|
|
22
|
+
if (debug) {
|
|
23
|
+
console.error(`[callAI:error] ${context} error:`, {
|
|
24
|
+
message: errorMessage,
|
|
25
|
+
status,
|
|
26
|
+
name: error?.name,
|
|
27
|
+
cause: error?.cause,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// Don't attempt API key refresh if explicitly skipped
|
|
31
|
+
if (options.skipRefresh) {
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
// Determine if this error suggests we need a new API key
|
|
35
|
+
const needsNewKey = (0, key_management_1.isNewKeyError)(error, debug);
|
|
36
|
+
// If the error suggests an API key issue, try to refresh the key
|
|
37
|
+
if (needsNewKey) {
|
|
38
|
+
if (debug) {
|
|
39
|
+
console.log(`[callAI:key-refresh] Error suggests API key issue, attempting refresh...`);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
// Use provided key/endpoint/refreshToken or fallback to global configuration
|
|
43
|
+
const currentKey = options.apiKey || key_management_1.keyStore.current;
|
|
44
|
+
const endpoint = options.endpoint || key_management_1.keyStore.refreshEndpoint;
|
|
45
|
+
let refreshToken = options.refreshToken || key_management_1.keyStore.refreshToken;
|
|
46
|
+
// First attempt to refresh the API key
|
|
47
|
+
try {
|
|
48
|
+
const { apiKey, topup } = await (0, key_management_1.refreshApiKey)(currentKey, endpoint, refreshToken, debug);
|
|
49
|
+
// Update the key in the store (if not already set by refreshApiKey)
|
|
50
|
+
if (key_management_1.keyStore.current !== apiKey) {
|
|
51
|
+
key_management_1.keyStore.current = apiKey;
|
|
52
|
+
}
|
|
53
|
+
if (debug) {
|
|
54
|
+
console.log(`[callAI:key-refresh] ${topup ? "Topped up" : "Refreshed"} API key successfully`);
|
|
55
|
+
}
|
|
56
|
+
// Return without throwing since we've successfully recovered
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
catch (initialRefreshError) {
|
|
60
|
+
// If there's an updateRefreshToken callback and the error was due to token issue
|
|
61
|
+
if (options.updateRefreshToken && refreshToken) {
|
|
62
|
+
if (debug) {
|
|
63
|
+
console.log(`[callAI:key-refresh] Initial refresh failed, attempting to update refresh token`);
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
// Get a new refresh token using the callback
|
|
67
|
+
const newRefreshToken = await options.updateRefreshToken(refreshToken);
|
|
68
|
+
if (newRefreshToken && newRefreshToken !== refreshToken) {
|
|
69
|
+
if (debug) {
|
|
70
|
+
console.log(`[callAI:key-refresh] Got new refresh token, retrying key refresh`);
|
|
71
|
+
}
|
|
72
|
+
// Update the stored refresh token
|
|
73
|
+
key_management_1.keyStore.refreshToken = newRefreshToken;
|
|
74
|
+
// Try again with the new token
|
|
75
|
+
const { apiKey, topup } = await (0, key_management_1.refreshApiKey)(currentKey, endpoint, newRefreshToken, debug);
|
|
76
|
+
// Update the key in the store
|
|
77
|
+
if (key_management_1.keyStore.current !== apiKey) {
|
|
78
|
+
key_management_1.keyStore.current = apiKey;
|
|
79
|
+
}
|
|
80
|
+
if (debug) {
|
|
81
|
+
console.log(`[callAI:key-refresh] ${topup ? "Topped up" : "Refreshed"} API key successfully with new refresh token`);
|
|
82
|
+
}
|
|
83
|
+
// Return without throwing since we've successfully recovered
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
if (debug) {
|
|
88
|
+
console.log(`[callAI:key-refresh] No new refresh token provided or same token returned, cannot retry`);
|
|
89
|
+
}
|
|
90
|
+
// Continue to error handling
|
|
91
|
+
throw initialRefreshError;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (tokenUpdateError) {
|
|
95
|
+
if (debug) {
|
|
96
|
+
console.error(`[callAI:key-refresh] Failed to update refresh token:`, tokenUpdateError);
|
|
97
|
+
}
|
|
98
|
+
// Continue to error handling with the original refresh error
|
|
99
|
+
throw initialRefreshError;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// No updateRefreshToken callback or no token, rethrow the initial error
|
|
104
|
+
throw initialRefreshError;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (refreshError) {
|
|
109
|
+
// Log refresh failure but throw the original error
|
|
110
|
+
if (debug) {
|
|
111
|
+
console.error(`[callAI:key-refresh] API key refresh failed:`, refreshError);
|
|
112
|
+
}
|
|
113
|
+
// Create a more detailed error from the original one
|
|
114
|
+
const detailedError = new Error(`${errorMessage} (Key refresh failed: ${refreshError instanceof Error ? refreshError.message : String(refreshError)})`);
|
|
115
|
+
// Preserve error metadata from the original error
|
|
116
|
+
detailedError.originalError = error;
|
|
117
|
+
detailedError.refreshError = refreshError;
|
|
118
|
+
detailedError.status = status || 401;
|
|
119
|
+
throw detailedError;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// For non-key errors, create a detailed error object
|
|
123
|
+
const detailedError = new Error(`${context}: ${errorMessage}`);
|
|
124
|
+
detailedError.originalError = error;
|
|
125
|
+
detailedError.status = status || 500;
|
|
126
|
+
detailedError.errorType = error?.name || "Error";
|
|
127
|
+
throw detailedError;
|
|
128
|
+
}
|
|
129
|
+
// Helper to check if an error indicates invalid model and handle fallback
|
|
130
|
+
async function checkForInvalidModelError(response, model, debug = key_management_1.globalDebug) {
|
|
131
|
+
// Only check 4xx errors (which could indicate invalid model)
|
|
132
|
+
if (response.status < 400 || response.status >= 500) {
|
|
133
|
+
return { isInvalidModel: false };
|
|
134
|
+
}
|
|
135
|
+
// Clone the response so we can still use the original later if needed
|
|
136
|
+
const responseClone = response.clone();
|
|
137
|
+
// Try to parse the response as JSON
|
|
138
|
+
let errorData;
|
|
139
|
+
try {
|
|
140
|
+
errorData = await responseClone.json();
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
// If it's not JSON, get the text
|
|
144
|
+
try {
|
|
145
|
+
const text = await responseClone.text();
|
|
146
|
+
errorData = { error: text };
|
|
147
|
+
}
|
|
148
|
+
catch (textError) {
|
|
149
|
+
errorData = { error: `Error ${response.status}: ${response.statusText}` };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Check if the error indicates an invalid model
|
|
153
|
+
const isInvalidModelError =
|
|
154
|
+
// Status checks
|
|
155
|
+
response.status === 404 ||
|
|
156
|
+
response.status === 400 ||
|
|
157
|
+
// Response content checks
|
|
158
|
+
(errorData &&
|
|
159
|
+
((typeof errorData.error === "string" &&
|
|
160
|
+
(errorData.error.toLowerCase().includes("model") ||
|
|
161
|
+
errorData.error.toLowerCase().includes("engine") ||
|
|
162
|
+
errorData.error.toLowerCase().includes("not found") ||
|
|
163
|
+
errorData.error.toLowerCase().includes("invalid") ||
|
|
164
|
+
errorData.error.toLowerCase().includes("unavailable"))) ||
|
|
165
|
+
(errorData.error?.message &&
|
|
166
|
+
typeof errorData.error.message === "string" &&
|
|
167
|
+
(errorData.error.message.toLowerCase().includes("model") ||
|
|
168
|
+
errorData.error.message.toLowerCase().includes("engine") ||
|
|
169
|
+
errorData.error.message.toLowerCase().includes("not found") ||
|
|
170
|
+
errorData.error.message.toLowerCase().includes("invalid") ||
|
|
171
|
+
errorData.error.message.toLowerCase().includes("unavailable")))));
|
|
172
|
+
if (debug && isInvalidModelError) {
|
|
173
|
+
console.log(`[callAI:model-fallback] Detected invalid model error for "${model}":`, errorData);
|
|
174
|
+
}
|
|
175
|
+
return { isInvalidModel: isInvalidModelError, errorData };
|
|
176
|
+
}
|
package/dist/image.js
CHANGED
|
@@ -19,18 +19,21 @@ async function imageGen(prompt, options = {}) {
|
|
|
19
19
|
// Get custom origin if set
|
|
20
20
|
const customOrigin = options.imgUrl ||
|
|
21
21
|
(typeof window !== "undefined" ? window.CALLAI_IMG_URL : null) ||
|
|
22
|
-
(typeof process !== "undefined" && process.env
|
|
22
|
+
(typeof process !== "undefined" && process.env
|
|
23
|
+
? process.env.CALLAI_IMG_URL
|
|
24
|
+
: null);
|
|
23
25
|
try {
|
|
24
26
|
// Handle image generation
|
|
25
27
|
if (!options.images || options.images.length === 0) {
|
|
26
28
|
// Simple image generation with text prompt
|
|
27
29
|
// Use custom origin or document.location.origin
|
|
28
|
-
const origin = customOrigin ||
|
|
30
|
+
const origin = customOrigin ||
|
|
31
|
+
(typeof document !== "undefined" ? document.location.origin : "");
|
|
29
32
|
const generateEndpoint = `${origin}/api/openai-image/generate`;
|
|
30
33
|
const response = await fetch(generateEndpoint, {
|
|
31
34
|
method: "POST",
|
|
32
35
|
headers: {
|
|
33
|
-
|
|
36
|
+
Authorization: `Bearer ${apiKey}`,
|
|
34
37
|
"Content-Type": "application/json",
|
|
35
38
|
},
|
|
36
39
|
body: JSON.stringify({
|
|
@@ -65,12 +68,13 @@ async function imageGen(prompt, options = {}) {
|
|
|
65
68
|
if (options.style)
|
|
66
69
|
formData.append("style", options.style);
|
|
67
70
|
// Use custom origin or document.location.origin
|
|
68
|
-
const origin = customOrigin ||
|
|
71
|
+
const origin = customOrigin ||
|
|
72
|
+
(typeof document !== "undefined" ? document.location.origin : "");
|
|
69
73
|
const editEndpoint = `${origin}/api/openai-image/edit`;
|
|
70
74
|
const response = await fetch(editEndpoint, {
|
|
71
75
|
method: "POST",
|
|
72
76
|
headers: {
|
|
73
|
-
|
|
77
|
+
Authorization: `Bearer ${apiKey}`,
|
|
74
78
|
},
|
|
75
79
|
body: formData,
|
|
76
80
|
});
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -17,12 +17,13 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
17
17
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
18
18
|
};
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.imageGen = exports.callAI = void 0;
|
|
20
|
+
exports.imageGen = exports.getMeta = exports.callAI = void 0;
|
|
21
21
|
// Export public types
|
|
22
22
|
__exportStar(require("./types"), exports);
|
|
23
|
-
// Export API
|
|
23
|
+
// Export API functions
|
|
24
24
|
var api_1 = require("./api");
|
|
25
25
|
Object.defineProperty(exports, "callAI", { enumerable: true, get: function () { return api_1.callAI; } });
|
|
26
|
+
Object.defineProperty(exports, "getMeta", { enumerable: true, get: function () { return api_1.getMeta; } });
|
|
26
27
|
// Export image generation function
|
|
27
28
|
var image_1 = require("./image");
|
|
28
29
|
Object.defineProperty(exports, "imageGen", { enumerable: true, get: function () { return image_1.imageGen; } });
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key management functionality for call-ai
|
|
3
|
+
*/
|
|
4
|
+
declare const keyStore: {
|
|
5
|
+
current: string | null;
|
|
6
|
+
refreshEndpoint: string | null;
|
|
7
|
+
refreshToken: string | null;
|
|
8
|
+
isRefreshing: boolean;
|
|
9
|
+
lastRefreshAttempt: number;
|
|
10
|
+
metadata: Record<string, any>;
|
|
11
|
+
};
|
|
12
|
+
declare let globalDebug: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Initialize key store with environment variables
|
|
15
|
+
*/
|
|
16
|
+
declare function initKeyStore(): void;
|
|
17
|
+
/**
|
|
18
|
+
* Check if an error indicates we need a new API key
|
|
19
|
+
* @param error The error to check
|
|
20
|
+
* @param debug Whether to log debug information
|
|
21
|
+
* @returns True if the error suggests we need a new key
|
|
22
|
+
*/
|
|
23
|
+
declare function isNewKeyError(error: any, debug?: boolean): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Refreshes the API key by calling the specified endpoint
|
|
26
|
+
* @param currentKey The current API key (may be null for initial key request)
|
|
27
|
+
* @param endpoint The endpoint to call for key refresh
|
|
28
|
+
* @param refreshToken Authentication token for the refresh endpoint
|
|
29
|
+
* @returns Object containing the API key and topup flag
|
|
30
|
+
*/
|
|
31
|
+
declare function refreshApiKey(currentKey: string | null, endpoint: string | null, refreshToken: string | null, debug?: boolean): Promise<{
|
|
32
|
+
apiKey: string;
|
|
33
|
+
topup: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Helper function to extract hash from key (implementation depends on how you store metadata)
|
|
37
|
+
*/
|
|
38
|
+
declare function getHashFromKey(key: string): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Helper function to store key metadata for future reference
|
|
41
|
+
*/
|
|
42
|
+
declare function storeKeyMetadata(data: any): void;
|
|
43
|
+
export { keyStore, globalDebug, initKeyStore, isNewKeyError, refreshApiKey, getHashFromKey, storeKeyMetadata, };
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Key management functionality for call-ai
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.globalDebug = exports.keyStore = void 0;
|
|
7
|
+
exports.initKeyStore = initKeyStore;
|
|
8
|
+
exports.isNewKeyError = isNewKeyError;
|
|
9
|
+
exports.refreshApiKey = refreshApiKey;
|
|
10
|
+
exports.getHashFromKey = getHashFromKey;
|
|
11
|
+
exports.storeKeyMetadata = storeKeyMetadata;
|
|
12
|
+
// Internal key store to keep track of the latest key
|
|
13
|
+
const keyStore = {
|
|
14
|
+
// Default key from environment or config
|
|
15
|
+
current: null,
|
|
16
|
+
// The refresh endpoint URL - defaults to vibecode.garden
|
|
17
|
+
refreshEndpoint: "https://vibecode.garden",
|
|
18
|
+
// Authentication token for refresh endpoint - defaults to use-vibes
|
|
19
|
+
refreshToken: "use-vibes",
|
|
20
|
+
// Flag to prevent concurrent refresh attempts
|
|
21
|
+
isRefreshing: false,
|
|
22
|
+
// Timestamp of last refresh attempt (to prevent too frequent refreshes)
|
|
23
|
+
lastRefreshAttempt: 0,
|
|
24
|
+
// Storage for key metadata (useful for future top-up implementation)
|
|
25
|
+
metadata: {},
|
|
26
|
+
};
|
|
27
|
+
exports.keyStore = keyStore;
|
|
28
|
+
// Global debug flag
|
|
29
|
+
let globalDebug = false;
|
|
30
|
+
exports.globalDebug = globalDebug;
|
|
31
|
+
/**
|
|
32
|
+
* Initialize key store with environment variables
|
|
33
|
+
*/
|
|
34
|
+
function initKeyStore() {
|
|
35
|
+
// Initialize with environment variables if available
|
|
36
|
+
if (typeof process !== "undefined" && process.env) {
|
|
37
|
+
if (process.env.CALLAI_API_KEY) {
|
|
38
|
+
keyStore.current = process.env.CALLAI_API_KEY;
|
|
39
|
+
}
|
|
40
|
+
// Support both CALLAI_REFRESH_ENDPOINT and CALLAI_REKEY_ENDPOINT for backward compatibility
|
|
41
|
+
if (process.env.CALLAI_REFRESH_ENDPOINT) {
|
|
42
|
+
keyStore.refreshEndpoint = process.env.CALLAI_REFRESH_ENDPOINT;
|
|
43
|
+
}
|
|
44
|
+
else if (process.env.CALLAI_REKEY_ENDPOINT) {
|
|
45
|
+
keyStore.refreshEndpoint = process.env.CALLAI_REKEY_ENDPOINT;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Default to vibecode.garden if not specified
|
|
49
|
+
keyStore.refreshEndpoint = "https://vibecode.garden";
|
|
50
|
+
}
|
|
51
|
+
// Support both CALL_AI_REFRESH_TOKEN and CALL_AI_KEY_TOKEN for backward compatibility
|
|
52
|
+
if (process.env.CALL_AI_REFRESH_TOKEN) {
|
|
53
|
+
keyStore.refreshToken = process.env.CALL_AI_REFRESH_TOKEN;
|
|
54
|
+
}
|
|
55
|
+
else if (process.env.CALL_AI_KEY_TOKEN) {
|
|
56
|
+
keyStore.refreshToken = process.env.CALL_AI_KEY_TOKEN;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Default to use-vibes if not specified - this is the default token for vibecode.garden
|
|
60
|
+
keyStore.refreshToken = "use-vibes";
|
|
61
|
+
}
|
|
62
|
+
// Check for CALLAI_DEBUG environment variable (any truthy value works)
|
|
63
|
+
if (process.env.CALLAI_DEBUG) {
|
|
64
|
+
// Set the global debug flag
|
|
65
|
+
exports.globalDebug = globalDebug = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Initialize from window globals if in browser context
|
|
69
|
+
else if (typeof window !== "undefined") {
|
|
70
|
+
// Use window.CALLAI_API_KEY or window.callAI.API_KEY if available
|
|
71
|
+
if (window.CALLAI_API_KEY) {
|
|
72
|
+
keyStore.current = window.CALLAI_API_KEY;
|
|
73
|
+
}
|
|
74
|
+
else if (window.callAI?.API_KEY) {
|
|
75
|
+
keyStore.current = window.callAI.API_KEY;
|
|
76
|
+
}
|
|
77
|
+
// Check for debug flag in browser environment
|
|
78
|
+
if (window.CALLAI_DEBUG) {
|
|
79
|
+
exports.globalDebug = globalDebug = true;
|
|
80
|
+
}
|
|
81
|
+
keyStore.refreshEndpoint =
|
|
82
|
+
window.CALLAI_REFRESH_ENDPOINT || keyStore.refreshEndpoint;
|
|
83
|
+
keyStore.refreshToken =
|
|
84
|
+
window.CALL_AI_REFRESH_TOKEN || keyStore.refreshToken;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Initialize on module load
|
|
88
|
+
initKeyStore();
|
|
89
|
+
/**
|
|
90
|
+
* Check if an error indicates we need a new API key
|
|
91
|
+
* @param error The error to check
|
|
92
|
+
* @param debug Whether to log debug information
|
|
93
|
+
* @returns True if the error suggests we need a new key
|
|
94
|
+
*/
|
|
95
|
+
function isNewKeyError(error, debug = false) {
|
|
96
|
+
// Extract status from error object or message text
|
|
97
|
+
let status = error?.status || error?.statusCode || error?.response?.status;
|
|
98
|
+
const errorMessage = String(error || "").toLowerCase();
|
|
99
|
+
// Extract status code from error message if not found in the object properties
|
|
100
|
+
// Handle messages like "HTTP error! Status: 403" common in fetch errors
|
|
101
|
+
if (!status && errorMessage.includes("status:")) {
|
|
102
|
+
const statusMatch = errorMessage.match(/status:\\s*(\\d+)/i);
|
|
103
|
+
if (statusMatch && statusMatch[1]) {
|
|
104
|
+
status = parseInt(statusMatch[1], 10);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const is4xx = status >= 400 && status < 500;
|
|
108
|
+
// Check for various error types that indicate key issues
|
|
109
|
+
const isAuthError = status === 401 ||
|
|
110
|
+
status === 403 ||
|
|
111
|
+
errorMessage.includes("unauthorized") ||
|
|
112
|
+
errorMessage.includes("forbidden") ||
|
|
113
|
+
errorMessage.includes("authentication") ||
|
|
114
|
+
errorMessage.includes("api key") ||
|
|
115
|
+
errorMessage.includes("apikey") ||
|
|
116
|
+
errorMessage.includes("auth");
|
|
117
|
+
// More specific message checks, especially for common API providers
|
|
118
|
+
const isInvalidKeyError = errorMessage.includes("invalid api key") ||
|
|
119
|
+
errorMessage.includes("invalid key") ||
|
|
120
|
+
errorMessage.includes("incorrect api key") ||
|
|
121
|
+
errorMessage.includes("incorrect key") ||
|
|
122
|
+
errorMessage.includes("authentication failed") ||
|
|
123
|
+
errorMessage.includes("not authorized");
|
|
124
|
+
// Check for OpenAI specific error patterns
|
|
125
|
+
const isOpenAIKeyError = errorMessage.includes("openai") &&
|
|
126
|
+
(errorMessage.includes("api key") ||
|
|
127
|
+
errorMessage.includes("authentication"));
|
|
128
|
+
// Check for rate limit errors which might indicate a key top-up is needed
|
|
129
|
+
const isRateLimitError = status === 429 ||
|
|
130
|
+
errorMessage.includes("rate limit") ||
|
|
131
|
+
errorMessage.includes("too many requests") ||
|
|
132
|
+
errorMessage.includes("quota") ||
|
|
133
|
+
errorMessage.includes("exceed");
|
|
134
|
+
// Check for billing or payment errors
|
|
135
|
+
const isBillingError = errorMessage.includes("billing") ||
|
|
136
|
+
errorMessage.includes("payment") ||
|
|
137
|
+
errorMessage.includes("subscription") ||
|
|
138
|
+
errorMessage.includes("account");
|
|
139
|
+
// Simple heuristic: if it's a 4xx error with any key-related terms, likely needs key refresh
|
|
140
|
+
const needsNewKey = is4xx &&
|
|
141
|
+
(isAuthError ||
|
|
142
|
+
isInvalidKeyError ||
|
|
143
|
+
isOpenAIKeyError ||
|
|
144
|
+
isRateLimitError ||
|
|
145
|
+
isBillingError);
|
|
146
|
+
if (debug && needsNewKey) {
|
|
147
|
+
console.log(`[callAI:key-refresh] Detected error requiring key refresh: ${errorMessage}`);
|
|
148
|
+
}
|
|
149
|
+
return needsNewKey;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Refreshes the API key by calling the specified endpoint
|
|
153
|
+
* @param currentKey The current API key (may be null for initial key request)
|
|
154
|
+
* @param endpoint The endpoint to call for key refresh
|
|
155
|
+
* @param refreshToken Authentication token for the refresh endpoint
|
|
156
|
+
* @returns Object containing the API key and topup flag
|
|
157
|
+
*/
|
|
158
|
+
async function refreshApiKey(currentKey, endpoint, refreshToken, debug = globalDebug) {
|
|
159
|
+
// Ensure we have an endpoint and refreshToken
|
|
160
|
+
if (!endpoint) {
|
|
161
|
+
throw new Error("No API key refresh endpoint specified");
|
|
162
|
+
}
|
|
163
|
+
if (!refreshToken) {
|
|
164
|
+
throw new Error("No API key refresh token specified");
|
|
165
|
+
}
|
|
166
|
+
// Check if we're already in the process of refreshing (to prevent parallel refreshes)
|
|
167
|
+
if (keyStore.isRefreshing) {
|
|
168
|
+
if (debug) {
|
|
169
|
+
console.log("API key refresh already in progress, waiting...");
|
|
170
|
+
}
|
|
171
|
+
// Wait for refresh to complete (simple polling)
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
const checkInterval = setInterval(() => {
|
|
174
|
+
if (!keyStore.isRefreshing && keyStore.current) {
|
|
175
|
+
clearInterval(checkInterval);
|
|
176
|
+
resolve({ apiKey: keyStore.current, topup: false });
|
|
177
|
+
}
|
|
178
|
+
}, 100);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// Rate limit key refresh to prevent overloading the service
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
const timeSinceLastRefresh = now - keyStore.lastRefreshAttempt;
|
|
184
|
+
const minRefreshInterval = 2000; // 2 seconds minimum interval between refreshes
|
|
185
|
+
if (timeSinceLastRefresh < minRefreshInterval) {
|
|
186
|
+
if (debug) {
|
|
187
|
+
console.log(`Rate limiting key refresh, last attempt was ${timeSinceLastRefresh}ms ago`);
|
|
188
|
+
}
|
|
189
|
+
// If we've refreshed too recently, wait a bit
|
|
190
|
+
await new Promise((resolve) => setTimeout(resolve, minRefreshInterval - timeSinceLastRefresh));
|
|
191
|
+
}
|
|
192
|
+
// Set refreshing flag and update last attempt timestamp
|
|
193
|
+
keyStore.isRefreshing = true;
|
|
194
|
+
keyStore.lastRefreshAttempt = Date.now();
|
|
195
|
+
// Process API paths
|
|
196
|
+
let apiPath = "/api/keys";
|
|
197
|
+
// Normalize endpoint URL to remove any trailing slashes
|
|
198
|
+
const baseUrl = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
|
|
199
|
+
// Construct the full URL
|
|
200
|
+
const url = `${baseUrl}${apiPath}`;
|
|
201
|
+
if (debug) {
|
|
202
|
+
console.log(`Refreshing API key from: ${url}`);
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
// Request payload
|
|
206
|
+
const requestPayload = {
|
|
207
|
+
key: currentKey,
|
|
208
|
+
hash: currentKey ? getHashFromKey(currentKey) : null,
|
|
209
|
+
name: "call-ai-client", // Add the required name field
|
|
210
|
+
};
|
|
211
|
+
if (debug) {
|
|
212
|
+
console.log(`[callAI:key-refresh] Request URL: ${url}`);
|
|
213
|
+
console.log(`[callAI:key-refresh] Request headers:`, {
|
|
214
|
+
"Content-Type": "application/json",
|
|
215
|
+
Authorization: `Bearer ${refreshToken}`,
|
|
216
|
+
});
|
|
217
|
+
console.log(`[callAI:key-refresh] Request payload:`, requestPayload);
|
|
218
|
+
}
|
|
219
|
+
// Make the request
|
|
220
|
+
const response = await fetch(url, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: {
|
|
223
|
+
"Content-Type": "application/json",
|
|
224
|
+
Authorization: `Bearer ${refreshToken}`,
|
|
225
|
+
},
|
|
226
|
+
body: JSON.stringify(requestPayload),
|
|
227
|
+
});
|
|
228
|
+
if (debug) {
|
|
229
|
+
console.log(`[callAI:key-refresh] Response status: ${response.status} ${response.statusText}`);
|
|
230
|
+
console.log(`[callAI:key-refresh] Response headers:`, Object.fromEntries([...response.headers.entries()]));
|
|
231
|
+
}
|
|
232
|
+
if (!response.ok) {
|
|
233
|
+
// Try to get the response body for more details
|
|
234
|
+
const errorText = await response.text();
|
|
235
|
+
if (debug) {
|
|
236
|
+
console.log(`[callAI:key-refresh] Error response body: ${errorText}`);
|
|
237
|
+
}
|
|
238
|
+
throw new Error(`API key refresh failed: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
|
|
239
|
+
}
|
|
240
|
+
// Parse the response
|
|
241
|
+
const data = await response.json();
|
|
242
|
+
// Log the complete response structure for debugging
|
|
243
|
+
if (debug) {
|
|
244
|
+
console.log(`[callAI:key-refresh] Full response structure:`, JSON.stringify(data, null, 2));
|
|
245
|
+
}
|
|
246
|
+
// Handle different API response formats
|
|
247
|
+
let newKey;
|
|
248
|
+
// Check if response has the new nested format with data.key.key
|
|
249
|
+
if (data.key && typeof data.key === "object" && data.key.key) {
|
|
250
|
+
newKey = data.key.key;
|
|
251
|
+
}
|
|
252
|
+
// Check for old format where data.key is the string key directly
|
|
253
|
+
else if (data.key && typeof data.key === "string") {
|
|
254
|
+
newKey = data.key;
|
|
255
|
+
}
|
|
256
|
+
// Handle error case
|
|
257
|
+
else {
|
|
258
|
+
throw new Error("Invalid response from key refresh endpoint: missing or malformed key");
|
|
259
|
+
}
|
|
260
|
+
if (debug) {
|
|
261
|
+
console.log(`API key refreshed successfully: ${newKey.substring(0, 10)}...`);
|
|
262
|
+
}
|
|
263
|
+
// Store metadata for potential future use (like top-up)
|
|
264
|
+
if (data.metadata ||
|
|
265
|
+
(data.key && typeof data.key === "object" && data.key.metadata)) {
|
|
266
|
+
const metadata = data.metadata || data.key.metadata;
|
|
267
|
+
storeKeyMetadata(metadata);
|
|
268
|
+
}
|
|
269
|
+
// Update the key store with the string value
|
|
270
|
+
keyStore.current = newKey;
|
|
271
|
+
// Determine if this was a top-up (using existing key) or new key
|
|
272
|
+
// For the new API response format, hash is in data.key.hash
|
|
273
|
+
const hashValue = data.hash || (data.key && typeof data.key === "object" && data.key.hash);
|
|
274
|
+
const isTopup = currentKey && hashValue && hashValue === getHashFromKey(currentKey);
|
|
275
|
+
// Reset refreshing flag
|
|
276
|
+
keyStore.isRefreshing = false;
|
|
277
|
+
return {
|
|
278
|
+
apiKey: newKey, // Return the string key, not the object
|
|
279
|
+
topup: isTopup,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
// Reset refreshing flag
|
|
284
|
+
keyStore.isRefreshing = false;
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Helper function to extract hash from key (implementation depends on how you store metadata)
|
|
290
|
+
*/
|
|
291
|
+
function getHashFromKey(key) {
|
|
292
|
+
if (!key)
|
|
293
|
+
return null;
|
|
294
|
+
// Simple implementation: just look up in our metadata store
|
|
295
|
+
const metaKey = Object.keys(keyStore.metadata).find((k) => k === key);
|
|
296
|
+
return metaKey ? keyStore.metadata[metaKey].hash || null : null;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Helper function to store key metadata for future reference
|
|
300
|
+
*/
|
|
301
|
+
function storeKeyMetadata(data) {
|
|
302
|
+
if (!data || !data.key)
|
|
303
|
+
return;
|
|
304
|
+
// Store metadata with the key as the dictionary key
|
|
305
|
+
keyStore.metadata[data.key] = {
|
|
306
|
+
hash: data.hash || null,
|
|
307
|
+
created: data.created || Date.now(),
|
|
308
|
+
expires: data.expires || null,
|
|
309
|
+
remaining: data.remaining || null,
|
|
310
|
+
limit: data.limit || null,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-streaming API call implementation for call-ai
|
|
3
|
+
*/
|
|
4
|
+
import { CallAIOptions, Message, SchemaStrategy } from "./types";
|
|
5
|
+
declare const PACKAGE_VERSION: any;
|
|
6
|
+
declare const FALLBACK_MODEL = "openrouter/auto";
|
|
7
|
+
declare function callAINonStreaming(prompt: string | Message[], options?: CallAIOptions, isRetry?: boolean): Promise<string>;
|
|
8
|
+
declare function extractContent(result: any, schemaStrategy: SchemaStrategy): any;
|
|
9
|
+
declare function extractClaudeResponse(response: Response): Promise<any>;
|
|
10
|
+
export { callAINonStreaming, extractContent, extractClaudeResponse, PACKAGE_VERSION, FALLBACK_MODEL, };
|