npm-ai-hooks 2.0.2 → 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/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
- getProvider(name) {
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
- // 1. If user specified provider and it's available
87
- if (name && this.providerFunctions.has(name)) {
88
- return { fn: this.providerFunctions.get(name), provider: name };
89
- }
90
- // 2. If default provider is specified and available
91
- if (this.defaultProvider && this.providerFunctions.has(this.defaultProvider)) {
92
- return { fn: this.providerFunctions.get(this.defaultProvider), provider: this.defaultProvider };
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
- // 3. Prefer OpenRouter if available
95
- if (this.providerFunctions.has('openrouter')) {
96
- return { fn: this.providerFunctions.get('openrouter'), provider: 'openrouter' };
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
- // 4. Use first available provider
99
- const firstProvider = available[0];
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 provider function and the actual provider name
46
- const { fn: providerFn, provider: providerKey } = (0, providers_1.getProvider)(options.provider);
47
- // Step 2: pick model: passed model or provider-specific default
48
- const model = options.model || (providerKey in types_1.DEFAULT_MODEL ? types_1.DEFAULT_MODEL[providerKey] : undefined);
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
- // Step 3: build prompt with multimodal support
53
- let prompt;
54
- if (options.customPrompt) {
55
- // Use custom prompt if provided
56
- prompt = `${options.customPrompt}\n\n${textInput}`;
57
- if (imageData) {
58
- prompt = `${prompt}\n\n[Image attached]`;
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
- else {
65
- // Use built-in task prompt
66
- prompt = buildPrompt(options.task, textInput, options.targetLanguage);
67
- if (imageData) {
68
- prompt = `${prompt}\n\n[Image attached]`;
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
- if (fileData) {
71
- prompt = `${prompt}\n\n[File: ${fileData.name}]`;
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
- const startTime = Date.now();
75
- let output;
76
- try {
77
- output = await providerFn(prompt, model);
78
- }
79
- catch (err) {
80
- if (err instanceof errors_1.AIHookError) {
81
- // For AIHookError, just re-throw it - it will be handled by the outer catch
82
- throw err;
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
- if (err instanceof Error) {
85
- throw new Error(`[ai-hooks] Unknown error calling provider: ${err.message}`);
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
- const endTime = Date.now();
90
- return {
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
- // For AIHookError, just log the pretty message without the full error handling
114
+ // Log the pretty message for developer visibility, then re-throw
104
115
  console.error(err.pretty());
105
- // Return a mock response to prevent the demo from crashing
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
- getProvider(name) {
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
- // 1. If user specified provider and it's available
84
- if (name && this.providerFunctions.has(name)) {
85
- return { fn: this.providerFunctions.get(name), provider: name };
86
- }
87
- // 2. If default provider is specified and available
88
- if (this.defaultProvider && this.providerFunctions.has(this.defaultProvider)) {
89
- return { fn: this.providerFunctions.get(this.defaultProvider), provider: this.defaultProvider };
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
- // 3. Prefer OpenRouter if available
92
- if (this.providerFunctions.has('openrouter')) {
93
- return { fn: this.providerFunctions.get('openrouter'), provider: 'openrouter' };
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
- // 4. Use first available provider
96
- const firstProvider = available[0];
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 { getProvider } from "./providers";
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 provider function and the actual provider name
43
- const { fn: providerFn, provider: providerKey } = getProvider(options.provider);
44
- // Step 2: pick model: passed model or provider-specific default
45
- const model = options.model || (providerKey in DEFAULT_MODEL ? DEFAULT_MODEL[providerKey] : undefined);
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
- // Step 3: build prompt with multimodal support
50
- let prompt;
51
- if (options.customPrompt) {
52
- // Use custom prompt if provided
53
- prompt = `${options.customPrompt}\n\n${textInput}`;
54
- if (imageData) {
55
- prompt = `${prompt}\n\n[Image attached]`;
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
- else {
62
- // Use built-in task prompt
63
- prompt = buildPrompt(options.task, textInput, options.targetLanguage);
64
- if (imageData) {
65
- prompt = `${prompt}\n\n[Image attached]`;
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
- if (fileData) {
68
- prompt = `${prompt}\n\n[File: ${fileData.name}]`;
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
- const startTime = Date.now();
72
- let output;
73
- try {
74
- output = await providerFn(prompt, model);
75
- }
76
- catch (err) {
77
- if (err instanceof AIHookError) {
78
- // For AIHookError, just re-throw it - it will be handled by the outer catch
79
- throw err;
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
- if (err instanceof Error) {
82
- throw new Error(`[ai-hooks] Unknown error calling provider: ${err.message}`);
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
- const endTime = Date.now();
87
- return {
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
- // For AIHookError, just log the pretty message without the full error handling
111
+ // Log the pretty message for developer visibility, then re-throw
101
112
  console.error(err.pretty());
102
- // Return a mock response to prevent the demo from crashing
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;
@@ -72,6 +72,7 @@ export declare class GeminiProvider extends BaseProvider {
72
72
  };
73
73
  formDataHeaderPolicy?: "legacy" | "content-only";
74
74
  redact?: string[];
75
+ sensitiveHeaders?: string[];
75
76
  };
76
77
  protected buildAuthHeaders(): Record<string, string>;
77
78
  }
@@ -7,5 +7,9 @@ export declare function getProvider(name?: Provider): {
7
7
  fn: ProviderFunction<any>;
8
8
  provider: Provider | "mock";
9
9
  };
10
+ export declare function getProviderChain(name?: Provider): Array<{
11
+ fn: ProviderFunction<any>;
12
+ provider: Provider;
13
+ }>;
10
14
  export { initAIHooks, addProvider, removeProvider, isInitialized, reset, UserProviderConfig, ProviderInitializationOptions };
11
15
  export { providerRegistry };
@@ -46,6 +46,15 @@ export declare function getProvider(name?: Provider): {
46
46
  fn: any;
47
47
  provider: Provider;
48
48
  };
49
+ /**
50
+ * Get the full ordered provider chain for fallback
51
+ * @param name - Optional preferred provider name (placed first)
52
+ * @returns Ordered array of {fn, provider} to try in sequence
53
+ */
54
+ export declare function getProviderChain(name?: Provider): Array<{
55
+ fn: any;
56
+ provider: Provider;
57
+ }>;
49
58
  /**
50
59
  * Check if AI hooks is initialized
51
60
  * @returns True if initialized
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npm-ai-hooks",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "Universal AI Hook Layer for Node.js and React – one wrapper for all AI providers. Inject LLM-like behavior into any JavaScript or TypeScript function with a single line, without writing prompts, handling SDKs, or locking into any provider.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -45,7 +45,7 @@
45
45
  "author": "AteebNoOne <ateebnoone@gmail.com>",
46
46
  "repository": {
47
47
  "type": "git",
48
- "url": "https://github.com/iTeebot/npm-ai-hooks.git"
48
+ "url": "git+https://github.com/iTeebot/npm-ai-hooks.git"
49
49
  },
50
50
  "license": "MIT",
51
51
  "dependencies": {
@@ -91,4 +91,4 @@
91
91
  "publishConfig": {
92
92
  "access": "public"
93
93
  }
94
- }
94
+ }