npm-ai-hooks 2.0.1 → 2.0.3
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/LICENSE +14 -14
- package/Readme.md +612 -612
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/providers/base/ProviderConfig.js +39 -14
- package/dist/cjs/providers/index.js +26 -0
- package/dist/cjs/providers/init.js +12 -0
- package/dist/cjs/wrap.js +61 -61
- package/dist/esm/index.js +1 -1
- package/dist/esm/providers/base/ProviderConfig.js +39 -14
- package/dist/esm/providers/index.js +26 -1
- package/dist/esm/providers/init.js +11 -0
- package/dist/esm/wrap.js +62 -62
- package/dist/index.d.ts +1 -1
- package/dist/providers/base/ProviderConfig.d.ts +9 -0
- package/dist/providers/base/SpecializedProviders.d.ts +19 -5
- package/dist/providers/index.d.ts +4 -0
- package/dist/providers/init.d.ts +9 -0
- package/package.json +94 -94
package/dist/cjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.quickValidateKeyFormat = exports.validateApiKeys = exports.validateApiKey = exports.getAllProviderInfo = exports.getProviderInfo = exports.getAllProviders = exports.getProviderModels = exports.PROVIDER_NAMES = exports.reset = exports.isInitialized = exports.getProvider = exports.getAvailableProviders = exports.removeProvider = exports.addProvider = exports.initAIHooks = exports.wrap = void 0;
|
|
3
|
+
exports.quickValidateKeyFormat = exports.validateApiKeys = exports.validateApiKey = exports.getAllProviderInfo = exports.getProviderInfo = exports.getAllProviders = exports.getProviderModels = exports.PROVIDER_NAMES = exports.reset = exports.isInitialized = exports.getProviderChain = exports.getProvider = exports.getAvailableProviders = exports.removeProvider = exports.addProvider = exports.initAIHooks = exports.wrap = void 0;
|
|
4
4
|
// src/index.ts
|
|
5
5
|
var wrap_1 = require("./wrap");
|
|
6
6
|
Object.defineProperty(exports, "wrap", { enumerable: true, get: function () { return wrap_1.wrap; } });
|
|
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "addProvider", { enumerable: true, get: function
|
|
|
10
10
|
Object.defineProperty(exports, "removeProvider", { enumerable: true, get: function () { return providers_1.removeProvider; } });
|
|
11
11
|
Object.defineProperty(exports, "getAvailableProviders", { enumerable: true, get: function () { return providers_1.getAvailableProviders; } });
|
|
12
12
|
Object.defineProperty(exports, "getProvider", { enumerable: true, get: function () { return providers_1.getProvider; } });
|
|
13
|
+
Object.defineProperty(exports, "getProviderChain", { enumerable: true, get: function () { return providers_1.getProviderChain; } });
|
|
13
14
|
Object.defineProperty(exports, "isInitialized", { enumerable: true, get: function () { return providers_1.isInitialized; } });
|
|
14
15
|
Object.defineProperty(exports, "reset", { enumerable: true, get: function () { return providers_1.reset; } });
|
|
15
16
|
// Export provider utilities
|
|
@@ -78,26 +78,51 @@ class ProviderManager {
|
|
|
78
78
|
getAvailableProviders() {
|
|
79
79
|
return Array.from(this.providers.keys());
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Returns the full ordered list of providers to try for a given request.
|
|
83
|
+
* Order: explicit name → defaultProvider → openrouter → rest in registration order.
|
|
84
|
+
* Used by the fallback mechanism in wrap().
|
|
85
|
+
*/
|
|
86
|
+
getProviderChain(name) {
|
|
82
87
|
const available = this.getAvailableProviders();
|
|
83
88
|
if (available.length === 0) {
|
|
84
89
|
throw new Error('No providers initialized. Please initialize providers first.');
|
|
85
90
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
const chain = [];
|
|
93
|
+
const push = (p) => {
|
|
94
|
+
if (!seen.has(p) && this.providerFunctions.has(p)) {
|
|
95
|
+
seen.add(p);
|
|
96
|
+
chain.push({ fn: this.providerFunctions.get(p), provider: p });
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
// 1. Explicitly requested provider goes first
|
|
100
|
+
if (name)
|
|
101
|
+
push(name);
|
|
102
|
+
// 2. Default provider next
|
|
103
|
+
if (this.defaultProvider)
|
|
104
|
+
push(this.defaultProvider);
|
|
105
|
+
// 3. OpenRouter preferred over generic providers
|
|
106
|
+
push('openrouter');
|
|
107
|
+
// 4. Rest in registration order
|
|
108
|
+
for (const p of available)
|
|
109
|
+
push(p);
|
|
110
|
+
return chain;
|
|
111
|
+
}
|
|
112
|
+
getProvider(name) {
|
|
113
|
+
const available = this.getAvailableProviders();
|
|
114
|
+
if (available.length === 0) {
|
|
115
|
+
throw new Error('No providers initialized. Please initialize providers first.');
|
|
93
116
|
}
|
|
94
|
-
//
|
|
95
|
-
if (
|
|
96
|
-
|
|
117
|
+
// If user explicitly requested a specific provider, it MUST be available
|
|
118
|
+
if (name) {
|
|
119
|
+
if (this.providerFunctions.has(name)) {
|
|
120
|
+
return { fn: this.providerFunctions.get(name), provider: name };
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`Provider "${name}" is not initialized. Available providers: ${available.join(', ')}`);
|
|
97
123
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return { fn: this.providerFunctions.get(firstProvider), provider: firstProvider };
|
|
124
|
+
const chain = this.getProviderChain();
|
|
125
|
+
return chain[0];
|
|
101
126
|
}
|
|
102
127
|
addProvider(config) {
|
|
103
128
|
this.providers.set(config.provider, {
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.providerRegistry = exports.reset = exports.isInitialized = exports.removeProvider = exports.addProvider = exports.initAIHooks = void 0;
|
|
4
4
|
exports.getAvailableProviders = getAvailableProviders;
|
|
5
5
|
exports.getProvider = getProvider;
|
|
6
|
+
exports.getProviderChain = getProviderChain;
|
|
6
7
|
const errors_1 = require("../errors");
|
|
7
8
|
const ProviderRegistry_1 = require("./base/ProviderRegistry");
|
|
8
9
|
Object.defineProperty(exports, "providerRegistry", { enumerable: true, get: function () { return ProviderRegistry_1.providerRegistry; } });
|
|
@@ -80,3 +81,28 @@ function getProvider(name) {
|
|
|
80
81
|
// 3. No valid keys found → throw error (single instruction, no fallback)
|
|
81
82
|
throw new errors_1.AIHookError("NO_PROVIDER_FOUND", "No valid AI provider API key was found.\n\nAt least one provider API key is required in your .env file.\n\nPlease add one of the following to your .env (see .env.example for details):\n - OPENAI_KEY\n - OPENROUTER_KEY\n - GROQ_KEY\n", undefined, "Reference .env.example for setup instructions.");
|
|
82
83
|
}
|
|
84
|
+
// Returns the full ordered provider chain for fallback (new system only)
|
|
85
|
+
function getProviderChain(name) {
|
|
86
|
+
if ((0, init_1.isInitialized)()) {
|
|
87
|
+
return (0, init_1.getProviderChain)(name);
|
|
88
|
+
}
|
|
89
|
+
// Legacy fallback: wrap available providers into a chain
|
|
90
|
+
const available = getAvailableProviders();
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
const chain = [];
|
|
93
|
+
const tryPush = (p) => {
|
|
94
|
+
if (!seen.has(p) && providers[p]) {
|
|
95
|
+
seen.add(p);
|
|
96
|
+
chain.push({ fn: providers[p], provider: p });
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
// Explicit name first
|
|
100
|
+
if (name)
|
|
101
|
+
tryPush(name);
|
|
102
|
+
// OpenRouter preferred
|
|
103
|
+
tryPush('openrouter');
|
|
104
|
+
// Rest in availability order
|
|
105
|
+
for (const p of available)
|
|
106
|
+
tryPush(p);
|
|
107
|
+
return chain;
|
|
108
|
+
}
|
|
@@ -5,6 +5,7 @@ exports.addProvider = addProvider;
|
|
|
5
5
|
exports.removeProvider = removeProvider;
|
|
6
6
|
exports.getAvailableProviders = getAvailableProviders;
|
|
7
7
|
exports.getProvider = getProvider;
|
|
8
|
+
exports.getProviderChain = getProviderChain;
|
|
8
9
|
exports.isInitialized = isInitialized;
|
|
9
10
|
exports.reset = reset;
|
|
10
11
|
const ProviderConfig_1 = require("./base/ProviderConfig");
|
|
@@ -75,6 +76,17 @@ function getProvider(name) {
|
|
|
75
76
|
}
|
|
76
77
|
return providerManager.getProvider(name);
|
|
77
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Get the full ordered provider chain for fallback
|
|
81
|
+
* @param name - Optional preferred provider name (placed first)
|
|
82
|
+
* @returns Ordered array of {fn, provider} to try in sequence
|
|
83
|
+
*/
|
|
84
|
+
function getProviderChain(name) {
|
|
85
|
+
if (!providerManager) {
|
|
86
|
+
throw new Error('AI hooks not initialized. Call initAIHooks() first.');
|
|
87
|
+
}
|
|
88
|
+
return providerManager.getProviderChain(name);
|
|
89
|
+
}
|
|
78
90
|
/**
|
|
79
91
|
* Check if AI hooks is initialized
|
|
80
92
|
* @returns True if initialized
|
package/dist/cjs/wrap.js
CHANGED
|
@@ -42,78 +42,78 @@ function wrap(fn, options) {
|
|
|
42
42
|
const textInput = isMultimodal ? input.text || '' : String(input);
|
|
43
43
|
const imageData = isMultimodal ? input.image : undefined;
|
|
44
44
|
const fileData = isMultimodal ? input.file : undefined;
|
|
45
|
-
// Step 1: get
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (!model) {
|
|
50
|
-
throw new errors_1.AIHookError("NO_MODEL_FOUND", "No model found: You must specify a provider or pass a valid model.\n\nAt least one provider API key is required in your .env file.\n\nPlease add one of the following to your .env (see .env.example for details):\n - OPENAI_KEY\n - OPENROUTER_KEY\n - GROQ_KEY\n", options.provider, "Reference .env.example for setup instructions.");
|
|
45
|
+
// Step 1: get the ordered provider chain for automatic fallback
|
|
46
|
+
const chain = (0, providers_1.getProviderChain)(options.provider);
|
|
47
|
+
if (chain.length === 0) {
|
|
48
|
+
throw new errors_1.AIHookError("NO_PROVIDER_FOUND", "No valid AI provider API key was found.\n\nAt least one provider API key is required.\n\nPlease call initAIHooks() with at least one provider configuration.", options.provider, "Call initAIHooks({ providers: [...] }) before using wrap().");
|
|
51
49
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (fileData) {
|
|
61
|
-
prompt = `${prompt}\n\n[File: ${fileData.name}]`;
|
|
50
|
+
let lastError;
|
|
51
|
+
const startTime = Date.now();
|
|
52
|
+
for (const { fn: providerFn, provider: providerKey } of chain) {
|
|
53
|
+
// Step 2: pick model: passed model or provider-specific default
|
|
54
|
+
const model = options.model || (providerKey in types_1.DEFAULT_MODEL ? types_1.DEFAULT_MODEL[providerKey] : undefined);
|
|
55
|
+
if (!model) {
|
|
56
|
+
// Skip providers with no resolvable model (shouldn't normally happen)
|
|
57
|
+
continue;
|
|
62
58
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
// Step 3: build prompt with multimodal support
|
|
60
|
+
let prompt;
|
|
61
|
+
if (options.customPrompt) {
|
|
62
|
+
prompt = `${options.customPrompt}\n\n${textInput}`;
|
|
63
|
+
if (imageData)
|
|
64
|
+
prompt = `${prompt}\n\n[Image attached]`;
|
|
65
|
+
if (fileData)
|
|
66
|
+
prompt = `${prompt}\n\n[File: ${fileData.name}]`;
|
|
69
67
|
}
|
|
70
|
-
|
|
71
|
-
prompt =
|
|
68
|
+
else {
|
|
69
|
+
prompt = buildPrompt(options.task, textInput, options.targetLanguage);
|
|
70
|
+
if (imageData)
|
|
71
|
+
prompt = `${prompt}\n\n[Image attached]`;
|
|
72
|
+
if (fileData)
|
|
73
|
+
prompt = `${prompt}\n\n[File: ${fileData.name}]`;
|
|
72
74
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
try {
|
|
76
|
+
const output = await providerFn(prompt, model);
|
|
77
|
+
const endTime = Date.now();
|
|
78
|
+
// Success — if we used a fallback provider, log it
|
|
79
|
+
if (options.provider && providerKey !== options.provider) {
|
|
80
|
+
console.warn(`[ai-hooks] ⚠️ Fell back to provider: ${providerKey} (original: ${options.provider} failed)`);
|
|
81
|
+
}
|
|
82
|
+
else if (!options.provider && chain[0].provider !== providerKey) {
|
|
83
|
+
console.warn(`[ai-hooks] ⚠️ Fell back to provider: ${providerKey}`);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
output,
|
|
87
|
+
meta: {
|
|
88
|
+
provider: providerKey,
|
|
89
|
+
model,
|
|
90
|
+
task: options.task,
|
|
91
|
+
targetLanguage: options.targetLanguage,
|
|
92
|
+
cached: false,
|
|
93
|
+
estimatedCostUSD: 0.0,
|
|
94
|
+
latencyMs: endTime - startTime,
|
|
95
|
+
fallback: chain[0].provider !== providerKey
|
|
96
|
+
}
|
|
97
|
+
};
|
|
83
98
|
}
|
|
84
|
-
|
|
85
|
-
|
|
99
|
+
catch (err) {
|
|
100
|
+
lastError = err;
|
|
101
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
102
|
+
// Only log warning if there are more providers to try
|
|
103
|
+
const idx = chain.findIndex(c => c.provider === providerKey);
|
|
104
|
+
if (idx < chain.length - 1) {
|
|
105
|
+
console.warn(`[ai-hooks] ⚠️ Provider "${providerKey}" failed (${errMsg}). Trying next provider...`);
|
|
106
|
+
}
|
|
86
107
|
}
|
|
87
|
-
throw new Error(`[ai-hooks] Unknown non-error thrown by provider: ${String(err)}`);
|
|
88
108
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
output,
|
|
92
|
-
meta: {
|
|
93
|
-
provider: providerKey,
|
|
94
|
-
model,
|
|
95
|
-
cached: false,
|
|
96
|
-
estimatedCostUSD: 0.0,
|
|
97
|
-
latencyMs: endTime - startTime
|
|
98
|
-
}
|
|
99
|
-
};
|
|
109
|
+
// All providers exhausted — surface the last error
|
|
110
|
+
throw lastError;
|
|
100
111
|
}
|
|
101
112
|
catch (err) {
|
|
102
113
|
if (err instanceof errors_1.AIHookError) {
|
|
103
|
-
//
|
|
114
|
+
// Log the pretty message for developer visibility, then re-throw
|
|
104
115
|
console.error(err.pretty());
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
output: "Error occurred",
|
|
108
|
-
meta: {
|
|
109
|
-
provider: "unknown",
|
|
110
|
-
model: "unknown",
|
|
111
|
-
cached: false,
|
|
112
|
-
estimatedCostUSD: 0.0,
|
|
113
|
-
latencyMs: 0,
|
|
114
|
-
error: true
|
|
115
|
-
}
|
|
116
|
-
};
|
|
116
|
+
throw err;
|
|
117
117
|
}
|
|
118
118
|
handleError(err);
|
|
119
119
|
}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
export { wrap } from "./wrap";
|
|
3
|
-
export { initAIHooks, addProvider, removeProvider, getAvailableProviders, getProvider, isInitialized, reset } from "./providers";
|
|
3
|
+
export { initAIHooks, addProvider, removeProvider, getAvailableProviders, getProvider, getProviderChain, isInitialized, reset } from "./providers";
|
|
4
4
|
// Export provider utilities
|
|
5
5
|
export { PROVIDER_NAMES, getProviderModels, getAllProviders, getProviderInfo, getAllProviderInfo } from "./utils/providerInfo";
|
|
6
6
|
// Export key validation utilities
|
|
@@ -75,26 +75,51 @@ export class ProviderManager {
|
|
|
75
75
|
getAvailableProviders() {
|
|
76
76
|
return Array.from(this.providers.keys());
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Returns the full ordered list of providers to try for a given request.
|
|
80
|
+
* Order: explicit name → defaultProvider → openrouter → rest in registration order.
|
|
81
|
+
* Used by the fallback mechanism in wrap().
|
|
82
|
+
*/
|
|
83
|
+
getProviderChain(name) {
|
|
79
84
|
const available = this.getAvailableProviders();
|
|
80
85
|
if (available.length === 0) {
|
|
81
86
|
throw new Error('No providers initialized. Please initialize providers first.');
|
|
82
87
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
const chain = [];
|
|
90
|
+
const push = (p) => {
|
|
91
|
+
if (!seen.has(p) && this.providerFunctions.has(p)) {
|
|
92
|
+
seen.add(p);
|
|
93
|
+
chain.push({ fn: this.providerFunctions.get(p), provider: p });
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
// 1. Explicitly requested provider goes first
|
|
97
|
+
if (name)
|
|
98
|
+
push(name);
|
|
99
|
+
// 2. Default provider next
|
|
100
|
+
if (this.defaultProvider)
|
|
101
|
+
push(this.defaultProvider);
|
|
102
|
+
// 3. OpenRouter preferred over generic providers
|
|
103
|
+
push('openrouter');
|
|
104
|
+
// 4. Rest in registration order
|
|
105
|
+
for (const p of available)
|
|
106
|
+
push(p);
|
|
107
|
+
return chain;
|
|
108
|
+
}
|
|
109
|
+
getProvider(name) {
|
|
110
|
+
const available = this.getAvailableProviders();
|
|
111
|
+
if (available.length === 0) {
|
|
112
|
+
throw new Error('No providers initialized. Please initialize providers first.');
|
|
90
113
|
}
|
|
91
|
-
//
|
|
92
|
-
if (
|
|
93
|
-
|
|
114
|
+
// If user explicitly requested a specific provider, it MUST be available
|
|
115
|
+
if (name) {
|
|
116
|
+
if (this.providerFunctions.has(name)) {
|
|
117
|
+
return { fn: this.providerFunctions.get(name), provider: name };
|
|
118
|
+
}
|
|
119
|
+
throw new Error(`Provider "${name}" is not initialized. Available providers: ${available.join(', ')}`);
|
|
94
120
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return { fn: this.providerFunctions.get(firstProvider), provider: firstProvider };
|
|
121
|
+
const chain = this.getProviderChain();
|
|
122
|
+
return chain[0];
|
|
98
123
|
}
|
|
99
124
|
addProvider(config) {
|
|
100
125
|
this.providers.set(config.provider, {
|
|
@@ -4,7 +4,7 @@ import { providerConfigs } from "./base/ProviderConfigs";
|
|
|
4
4
|
import { BaseProvider } from "./base/BaseProvider";
|
|
5
5
|
import { ClaudeProvider, GeminiProvider, OpenRouterProvider } from "./base/SpecializedProviders";
|
|
6
6
|
// Import the new initialization system
|
|
7
|
-
import { initAIHooks, addProvider, removeProvider, getAvailableProviders as getNewAvailableProviders, getProvider as getNewProvider, isInitialized, reset } from "./init";
|
|
7
|
+
import { initAIHooks, addProvider, removeProvider, getAvailableProviders as getNewAvailableProviders, getProvider as getNewProvider, getProviderChain as getNewProviderChain, isInitialized, reset } from "./init";
|
|
8
8
|
// Initialize all providers (legacy environment-based system)
|
|
9
9
|
function initializeProviders() {
|
|
10
10
|
// Standard providers using BaseProvider
|
|
@@ -69,6 +69,31 @@ export function getProvider(name) {
|
|
|
69
69
|
// 3. No valid keys found → throw error (single instruction, no fallback)
|
|
70
70
|
throw new AIHookError("NO_PROVIDER_FOUND", "No valid AI provider API key was found.\n\nAt least one provider API key is required in your .env file.\n\nPlease add one of the following to your .env (see .env.example for details):\n - OPENAI_KEY\n - OPENROUTER_KEY\n - GROQ_KEY\n", undefined, "Reference .env.example for setup instructions.");
|
|
71
71
|
}
|
|
72
|
+
// Returns the full ordered provider chain for fallback (new system only)
|
|
73
|
+
export function getProviderChain(name) {
|
|
74
|
+
if (isInitialized()) {
|
|
75
|
+
return getNewProviderChain(name);
|
|
76
|
+
}
|
|
77
|
+
// Legacy fallback: wrap available providers into a chain
|
|
78
|
+
const available = getAvailableProviders();
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
const chain = [];
|
|
81
|
+
const tryPush = (p) => {
|
|
82
|
+
if (!seen.has(p) && providers[p]) {
|
|
83
|
+
seen.add(p);
|
|
84
|
+
chain.push({ fn: providers[p], provider: p });
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
// Explicit name first
|
|
88
|
+
if (name)
|
|
89
|
+
tryPush(name);
|
|
90
|
+
// OpenRouter preferred
|
|
91
|
+
tryPush('openrouter');
|
|
92
|
+
// Rest in availability order
|
|
93
|
+
for (const p of available)
|
|
94
|
+
tryPush(p);
|
|
95
|
+
return chain;
|
|
96
|
+
}
|
|
72
97
|
// Export the new initialization system
|
|
73
98
|
export { initAIHooks, addProvider, removeProvider, isInitialized, reset };
|
|
74
99
|
// Export the registry for advanced usage (legacy)
|
|
@@ -66,6 +66,17 @@ export function getProvider(name) {
|
|
|
66
66
|
}
|
|
67
67
|
return providerManager.getProvider(name);
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Get the full ordered provider chain for fallback
|
|
71
|
+
* @param name - Optional preferred provider name (placed first)
|
|
72
|
+
* @returns Ordered array of {fn, provider} to try in sequence
|
|
73
|
+
*/
|
|
74
|
+
export function getProviderChain(name) {
|
|
75
|
+
if (!providerManager) {
|
|
76
|
+
throw new Error('AI hooks not initialized. Call initAIHooks() first.');
|
|
77
|
+
}
|
|
78
|
+
return providerManager.getProviderChain(name);
|
|
79
|
+
}
|
|
69
80
|
/**
|
|
70
81
|
* Check if AI hooks is initialized
|
|
71
82
|
* @returns True if initialized
|
package/dist/esm/wrap.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// dotenv removed - using explicit provider initialization instead
|
|
2
|
-
import {
|
|
2
|
+
import { getProviderChain } from "./providers";
|
|
3
3
|
import { DEFAULT_MODEL } from "./types";
|
|
4
4
|
import { AIHookError } from "./errors";
|
|
5
5
|
function handleError(err) {
|
|
@@ -39,78 +39,78 @@ export function wrap(fn, options) {
|
|
|
39
39
|
const textInput = isMultimodal ? input.text || '' : String(input);
|
|
40
40
|
const imageData = isMultimodal ? input.image : undefined;
|
|
41
41
|
const fileData = isMultimodal ? input.file : undefined;
|
|
42
|
-
// Step 1: get
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!model) {
|
|
47
|
-
throw new AIHookError("NO_MODEL_FOUND", "No model found: You must specify a provider or pass a valid model.\n\nAt least one provider API key is required in your .env file.\n\nPlease add one of the following to your .env (see .env.example for details):\n - OPENAI_KEY\n - OPENROUTER_KEY\n - GROQ_KEY\n", options.provider, "Reference .env.example for setup instructions.");
|
|
42
|
+
// Step 1: get the ordered provider chain for automatic fallback
|
|
43
|
+
const chain = getProviderChain(options.provider);
|
|
44
|
+
if (chain.length === 0) {
|
|
45
|
+
throw new AIHookError("NO_PROVIDER_FOUND", "No valid AI provider API key was found.\n\nAt least one provider API key is required.\n\nPlease call initAIHooks() with at least one provider configuration.", options.provider, "Call initAIHooks({ providers: [...] }) before using wrap().");
|
|
48
46
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (fileData) {
|
|
58
|
-
prompt = `${prompt}\n\n[File: ${fileData.name}]`;
|
|
47
|
+
let lastError;
|
|
48
|
+
const startTime = Date.now();
|
|
49
|
+
for (const { fn: providerFn, provider: providerKey } of chain) {
|
|
50
|
+
// Step 2: pick model: passed model or provider-specific default
|
|
51
|
+
const model = options.model || (providerKey in DEFAULT_MODEL ? DEFAULT_MODEL[providerKey] : undefined);
|
|
52
|
+
if (!model) {
|
|
53
|
+
// Skip providers with no resolvable model (shouldn't normally happen)
|
|
54
|
+
continue;
|
|
59
55
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
56
|
+
// Step 3: build prompt with multimodal support
|
|
57
|
+
let prompt;
|
|
58
|
+
if (options.customPrompt) {
|
|
59
|
+
prompt = `${options.customPrompt}\n\n${textInput}`;
|
|
60
|
+
if (imageData)
|
|
61
|
+
prompt = `${prompt}\n\n[Image attached]`;
|
|
62
|
+
if (fileData)
|
|
63
|
+
prompt = `${prompt}\n\n[File: ${fileData.name}]`;
|
|
66
64
|
}
|
|
67
|
-
|
|
68
|
-
prompt =
|
|
65
|
+
else {
|
|
66
|
+
prompt = buildPrompt(options.task, textInput, options.targetLanguage);
|
|
67
|
+
if (imageData)
|
|
68
|
+
prompt = `${prompt}\n\n[Image attached]`;
|
|
69
|
+
if (fileData)
|
|
70
|
+
prompt = `${prompt}\n\n[File: ${fileData.name}]`;
|
|
69
71
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
try {
|
|
73
|
+
const output = await providerFn(prompt, model);
|
|
74
|
+
const endTime = Date.now();
|
|
75
|
+
// Success — if we used a fallback provider, log it
|
|
76
|
+
if (options.provider && providerKey !== options.provider) {
|
|
77
|
+
console.warn(`[ai-hooks] ⚠️ Fell back to provider: ${providerKey} (original: ${options.provider} failed)`);
|
|
78
|
+
}
|
|
79
|
+
else if (!options.provider && chain[0].provider !== providerKey) {
|
|
80
|
+
console.warn(`[ai-hooks] ⚠️ Fell back to provider: ${providerKey}`);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
output,
|
|
84
|
+
meta: {
|
|
85
|
+
provider: providerKey,
|
|
86
|
+
model,
|
|
87
|
+
task: options.task,
|
|
88
|
+
targetLanguage: options.targetLanguage,
|
|
89
|
+
cached: false,
|
|
90
|
+
estimatedCostUSD: 0.0,
|
|
91
|
+
latencyMs: endTime - startTime,
|
|
92
|
+
fallback: chain[0].provider !== providerKey
|
|
93
|
+
}
|
|
94
|
+
};
|
|
80
95
|
}
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
catch (err) {
|
|
97
|
+
lastError = err;
|
|
98
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
99
|
+
// Only log warning if there are more providers to try
|
|
100
|
+
const idx = chain.findIndex(c => c.provider === providerKey);
|
|
101
|
+
if (idx < chain.length - 1) {
|
|
102
|
+
console.warn(`[ai-hooks] ⚠️ Provider "${providerKey}" failed (${errMsg}). Trying next provider...`);
|
|
103
|
+
}
|
|
83
104
|
}
|
|
84
|
-
throw new Error(`[ai-hooks] Unknown non-error thrown by provider: ${String(err)}`);
|
|
85
105
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
output,
|
|
89
|
-
meta: {
|
|
90
|
-
provider: providerKey,
|
|
91
|
-
model,
|
|
92
|
-
cached: false,
|
|
93
|
-
estimatedCostUSD: 0.0,
|
|
94
|
-
latencyMs: endTime - startTime
|
|
95
|
-
}
|
|
96
|
-
};
|
|
106
|
+
// All providers exhausted — surface the last error
|
|
107
|
+
throw lastError;
|
|
97
108
|
}
|
|
98
109
|
catch (err) {
|
|
99
110
|
if (err instanceof AIHookError) {
|
|
100
|
-
//
|
|
111
|
+
// Log the pretty message for developer visibility, then re-throw
|
|
101
112
|
console.error(err.pretty());
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
output: "Error occurred",
|
|
105
|
-
meta: {
|
|
106
|
-
provider: "unknown",
|
|
107
|
-
model: "unknown",
|
|
108
|
-
cached: false,
|
|
109
|
-
estimatedCostUSD: 0.0,
|
|
110
|
-
latencyMs: 0,
|
|
111
|
-
error: true
|
|
112
|
-
}
|
|
113
|
-
};
|
|
113
|
+
throw err;
|
|
114
114
|
}
|
|
115
115
|
handleError(err);
|
|
116
116
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { wrap } from "./wrap";
|
|
2
2
|
export type { MultimodalInput } from "./wrap";
|
|
3
|
-
export { initAIHooks, addProvider, removeProvider, getAvailableProviders, getProvider, isInitialized, reset } from "./providers";
|
|
3
|
+
export { initAIHooks, addProvider, removeProvider, getAvailableProviders, getProvider, getProviderChain, isInitialized, reset } from "./providers";
|
|
4
4
|
export type { Provider, ProviderModels, TaskType } from "./types";
|
|
5
5
|
export type { OpenAIModel, ClaudeModel, GeminiModel, GroqModel, DeepSeekModel, XAIModel, MistralModel, PerplexityModel, OpenRouterModel } from "./types";
|
|
6
6
|
export { PROVIDER_NAMES, getProviderModels, getAllProviders, getProviderInfo, getAllProviderInfo, type ProviderInfo } from "./utils/providerInfo";
|
|
@@ -19,6 +19,15 @@ export declare class ProviderManager {
|
|
|
19
19
|
private createProviderInstance;
|
|
20
20
|
private getDefaultModelForProvider;
|
|
21
21
|
getAvailableProviders(): Provider[];
|
|
22
|
+
/**
|
|
23
|
+
* Returns the full ordered list of providers to try for a given request.
|
|
24
|
+
* Order: explicit name → defaultProvider → openrouter → rest in registration order.
|
|
25
|
+
* Used by the fallback mechanism in wrap().
|
|
26
|
+
*/
|
|
27
|
+
getProviderChain(name?: Provider): Array<{
|
|
28
|
+
fn: any;
|
|
29
|
+
provider: Provider;
|
|
30
|
+
}>;
|
|
22
31
|
getProvider(name?: Provider): {
|
|
23
32
|
fn: any;
|
|
24
33
|
provider: Provider;
|