npm-ai-hooks 2.0.0 → 2.0.2

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.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.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; } });
@@ -12,3 +12,15 @@ Object.defineProperty(exports, "getAvailableProviders", { enumerable: true, get:
12
12
  Object.defineProperty(exports, "getProvider", { enumerable: true, get: function () { return providers_1.getProvider; } });
13
13
  Object.defineProperty(exports, "isInitialized", { enumerable: true, get: function () { return providers_1.isInitialized; } });
14
14
  Object.defineProperty(exports, "reset", { enumerable: true, get: function () { return providers_1.reset; } });
15
+ // Export provider utilities
16
+ var providerInfo_1 = require("./utils/providerInfo");
17
+ Object.defineProperty(exports, "PROVIDER_NAMES", { enumerable: true, get: function () { return providerInfo_1.PROVIDER_NAMES; } });
18
+ Object.defineProperty(exports, "getProviderModels", { enumerable: true, get: function () { return providerInfo_1.getProviderModels; } });
19
+ Object.defineProperty(exports, "getAllProviders", { enumerable: true, get: function () { return providerInfo_1.getAllProviders; } });
20
+ Object.defineProperty(exports, "getProviderInfo", { enumerable: true, get: function () { return providerInfo_1.getProviderInfo; } });
21
+ Object.defineProperty(exports, "getAllProviderInfo", { enumerable: true, get: function () { return providerInfo_1.getAllProviderInfo; } });
22
+ // Export key validation utilities
23
+ var keyValidator_1 = require("./utils/keyValidator");
24
+ Object.defineProperty(exports, "validateApiKey", { enumerable: true, get: function () { return keyValidator_1.validateApiKey; } });
25
+ Object.defineProperty(exports, "validateApiKeys", { enumerable: true, get: function () { return keyValidator_1.validateApiKeys; } });
26
+ Object.defineProperty(exports, "quickValidateKeyFormat", { enumerable: true, get: function () { return keyValidator_1.quickValidateKeyFormat; } });
@@ -29,7 +29,7 @@ exports.providerConfigs = {
29
29
  openai: {
30
30
  name: "openai",
31
31
  baseUrl: "https://api.openai.com/v1/chat/completions",
32
- envKey: "AI_HOOK_OPENAI_KEY",
32
+ envKey: "OPENAI_KEY",
33
33
  headers: { "Content-Type": "application/json" },
34
34
  requestBody: exports.requestBodyBuilders.openaiStyle,
35
35
  responseParser: exports.responseParsers.openaiStyle,
@@ -46,7 +46,7 @@ exports.providerConfigs = {
46
46
  groq: {
47
47
  name: "groq",
48
48
  baseUrl: "https://api.groq.com/openai/v1/chat/completions",
49
- envKey: "AI_HOOK_GROQ_KEY",
49
+ envKey: "GROQ_KEY",
50
50
  headers: { "Content-Type": "application/json" },
51
51
  requestBody: exports.requestBodyBuilders.openaiStyle,
52
52
  responseParser: exports.responseParsers.openaiStyle,
@@ -63,7 +63,7 @@ exports.providerConfigs = {
63
63
  claude: {
64
64
  name: "claude",
65
65
  baseUrl: "https://api.anthropic.com/v1/messages",
66
- envKey: "AI_HOOK_CLAUDE_KEY",
66
+ envKey: "CLAUDE_KEY",
67
67
  headers: {
68
68
  "Content-Type": "application/json",
69
69
  "anthropic-version": "2023-06-01"
@@ -83,7 +83,7 @@ exports.providerConfigs = {
83
83
  gemini: {
84
84
  name: "gemini",
85
85
  baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
86
- envKey: "AI_HOOK_GEMINI_KEY",
86
+ envKey: "GEMINI_KEY",
87
87
  headers: { "Content-Type": "application/json" },
88
88
  requestBody: exports.requestBodyBuilders.geminiStyle,
89
89
  responseParser: exports.responseParsers.geminiStyle,
@@ -100,7 +100,7 @@ exports.providerConfigs = {
100
100
  deepseek: {
101
101
  name: "deepseek",
102
102
  baseUrl: "https://api.deepseek.com/v1/chat/completions",
103
- envKey: "AI_HOOK_DEEPSEEK_KEY",
103
+ envKey: "DEEPSEEK_KEY",
104
104
  headers: { "Content-Type": "application/json" },
105
105
  requestBody: exports.requestBodyBuilders.openaiStyle,
106
106
  responseParser: exports.responseParsers.openaiStyle,
@@ -117,7 +117,7 @@ exports.providerConfigs = {
117
117
  mistral: {
118
118
  name: "mistral",
119
119
  baseUrl: "https://api.mistral.ai/v1/chat/completions",
120
- envKey: "AI_HOOK_MISTRAL_KEY",
120
+ envKey: "MISTRAL_KEY",
121
121
  headers: { "Content-Type": "application/json" },
122
122
  requestBody: exports.requestBodyBuilders.openaiStyle,
123
123
  responseParser: exports.responseParsers.openaiStyle,
@@ -134,7 +134,7 @@ exports.providerConfigs = {
134
134
  xai: {
135
135
  name: "xai",
136
136
  baseUrl: "https://api.x.ai/v1/chat/completions",
137
- envKey: "AI_HOOK_XAI_KEY",
137
+ envKey: "XAI_KEY",
138
138
  headers: { "Content-Type": "application/json" },
139
139
  requestBody: exports.requestBodyBuilders.openaiStyle,
140
140
  responseParser: exports.responseParsers.openaiStyle,
@@ -151,7 +151,7 @@ exports.providerConfigs = {
151
151
  perplexity: {
152
152
  name: "perplexity",
153
153
  baseUrl: "https://api.perplexity.ai/chat/completions",
154
- envKey: "AI_HOOK_PERPLEXITY_KEY",
154
+ envKey: "PERPLEXITY_KEY",
155
155
  headers: { "Content-Type": "application/json" },
156
156
  requestBody: exports.requestBodyBuilders.openaiStyle,
157
157
  responseParser: exports.responseParsers.openaiStyle,
@@ -168,7 +168,7 @@ exports.providerConfigs = {
168
168
  openrouter: {
169
169
  name: "openrouter",
170
170
  baseUrl: "https://openrouter.ai/api/v1/chat/completions",
171
- envKey: "AI_HOOK_OPENROUTER_KEY",
171
+ envKey: "OPENROUTER_KEY",
172
172
  headers: { "Content-Type": "application/json" },
173
173
  requestBody: exports.requestBodyBuilders.openaiStyle,
174
174
  responseParser: exports.responseParsers.openaiStyle,
@@ -78,5 +78,5 @@ function getProvider(name) {
78
78
  return { fn: providers[available[0]], provider: available[0] };
79
79
  }
80
80
  // 3. No valid keys found → throw error (single instruction, no fallback)
81
- 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 - AI_HOOK_OPENAI_KEY\n - AI_HOOK_OPENROUTER_KEY\n - AI_HOOK_GROQ_KEY\n", undefined, "Reference .env.example for setup instructions.");
81
+ 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
82
  }
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateApiKey = validateApiKey;
4
+ exports.validateApiKeys = validateApiKeys;
5
+ exports.quickValidateKeyFormat = quickValidateKeyFormat;
6
+ const BaseProvider_1 = require("../providers/base/BaseProvider");
7
+ const SpecializedProviders_1 = require("../providers/base/SpecializedProviders");
8
+ const ProviderConfigs_1 = require("../providers/base/ProviderConfigs");
9
+ /**
10
+ * Validates an API key by making a minimal test request to the provider
11
+ */
12
+ async function validateApiKey(provider, apiKey) {
13
+ if (!apiKey || apiKey.trim() === '') {
14
+ return {
15
+ valid: false,
16
+ provider,
17
+ error: 'EMPTY_KEY',
18
+ message: 'API key is empty'
19
+ };
20
+ }
21
+ try {
22
+ let providerInstance;
23
+ // Create provider instance based on type
24
+ switch (provider) {
25
+ case 'claude':
26
+ providerInstance = new SpecializedProviders_1.ClaudeProvider();
27
+ break;
28
+ case 'gemini':
29
+ providerInstance = new SpecializedProviders_1.GeminiProvider();
30
+ break;
31
+ case 'openrouter':
32
+ providerInstance = new SpecializedProviders_1.OpenRouterProvider();
33
+ break;
34
+ default:
35
+ const config = ProviderConfigs_1.providerConfigs[provider];
36
+ if (!config) {
37
+ return {
38
+ valid: false,
39
+ provider,
40
+ error: 'UNKNOWN_PROVIDER',
41
+ message: `Unknown provider: ${provider}`
42
+ };
43
+ }
44
+ providerInstance = new BaseProvider_1.BaseProvider(config);
45
+ }
46
+ // Set the API key temporarily
47
+ providerInstance.apiKey = apiKey;
48
+ // Make a minimal test request
49
+ const testPrompt = "Hi";
50
+ const testModel = getDefaultModel(provider);
51
+ await providerInstance.call(testPrompt, testModel);
52
+ return {
53
+ valid: true,
54
+ provider,
55
+ message: 'API key is valid'
56
+ };
57
+ }
58
+ catch (error) {
59
+ // Check for common error types
60
+ if (error.message?.includes('401') || error.message?.includes('Unauthorized') || error.message?.includes('Invalid API key')) {
61
+ return {
62
+ valid: false,
63
+ provider,
64
+ error: 'INVALID_KEY',
65
+ message: 'API key is invalid or unauthorized'
66
+ };
67
+ }
68
+ if (error.message?.includes('403') || error.message?.includes('Forbidden')) {
69
+ return {
70
+ valid: false,
71
+ provider,
72
+ error: 'FORBIDDEN',
73
+ message: 'API key does not have access to this resource'
74
+ };
75
+ }
76
+ if (error.message?.includes('429') || error.message?.includes('rate limit')) {
77
+ // Rate limit means the key is valid, just too many requests
78
+ return {
79
+ valid: true,
80
+ provider,
81
+ message: 'API key is valid (rate limited)'
82
+ };
83
+ }
84
+ // If we get other errors, the key might still be valid (could be model/format issues)
85
+ return {
86
+ valid: true, // Assume valid unless we get clear auth error
87
+ provider,
88
+ message: 'API key appears valid (test request had non-auth error)'
89
+ };
90
+ }
91
+ }
92
+ /**
93
+ * Validates multiple API keys
94
+ */
95
+ async function validateApiKeys(keys) {
96
+ const results = {};
97
+ for (const [provider, key] of Object.entries(keys)) {
98
+ if (key && key.trim()) {
99
+ results[provider] = await validateApiKey(provider, key);
100
+ }
101
+ }
102
+ return results;
103
+ }
104
+ /**
105
+ * Get default model for a provider (for testing)
106
+ */
107
+ function getDefaultModel(provider) {
108
+ const defaultModels = {
109
+ openai: 'gpt-3.5-turbo',
110
+ claude: 'claude-3-haiku-20240307',
111
+ gemini: 'gemini-1.5-flash',
112
+ groq: 'llama-3.1-8b-instant',
113
+ deepseek: 'deepseek-chat',
114
+ xai: 'grok-beta',
115
+ mistral: 'mistral-small-latest',
116
+ perplexity: 'llama-3.1-sonar-small-128k-chat',
117
+ openrouter: 'openai/gpt-3.5-turbo'
118
+ };
119
+ return defaultModels[provider] || 'default';
120
+ }
121
+ /**
122
+ * Quick check if API key format looks valid (doesn't make API call)
123
+ */
124
+ function quickValidateKeyFormat(provider, apiKey) {
125
+ if (!apiKey || apiKey.trim() === '') {
126
+ return {
127
+ valid: false,
128
+ provider,
129
+ error: 'EMPTY_KEY',
130
+ message: 'API key is empty'
131
+ };
132
+ }
133
+ // Check key format patterns
134
+ const patterns = {
135
+ openai: /^sk-[a-zA-Z0-9-_]{20,}$/,
136
+ claude: /^sk-ant-[a-zA-Z0-9-_]{20,}$/,
137
+ gemini: /^AIza[a-zA-Z0-9-_]{35}$/,
138
+ groq: /^gsk_[a-zA-Z0-9]{52}$/,
139
+ deepseek: /^sk-[a-zA-Z0-9]{32}$/,
140
+ xai: /^xai-[a-zA-Z0-9-_]{40,}$/,
141
+ mistral: /^[a-zA-Z0-9]{32}$/,
142
+ perplexity: /^pplx-[a-zA-Z0-9]{40}$/,
143
+ openrouter: /^sk-or-v1-[a-zA-Z0-9]{64}$/
144
+ };
145
+ const pattern = patterns[provider];
146
+ if (pattern && !pattern.test(apiKey)) {
147
+ return {
148
+ valid: false,
149
+ provider,
150
+ error: 'INVALID_FORMAT',
151
+ message: `API key format doesn't match expected pattern for ${provider}`
152
+ };
153
+ }
154
+ return {
155
+ valid: true,
156
+ provider,
157
+ message: 'API key format looks valid'
158
+ };
159
+ }
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PROVIDER_NAMES = void 0;
4
+ exports.getProviderModels = getProviderModels;
5
+ exports.getAllProviders = getAllProviders;
6
+ exports.getProviderInfo = getProviderInfo;
7
+ exports.getAllProviderInfo = getAllProviderInfo;
8
+ // Provider display names
9
+ exports.PROVIDER_NAMES = {
10
+ openai: "OpenAI",
11
+ claude: "Claude",
12
+ gemini: "Gemini",
13
+ groq: "Groq",
14
+ deepseek: "DeepSeek",
15
+ xai: "xAI",
16
+ mistral: "Mistral",
17
+ perplexity: "Perplexity",
18
+ openrouter: "OpenRouter"
19
+ };
20
+ // Get all available models for a provider
21
+ function getProviderModels(provider) {
22
+ // These are the actual model types from the library
23
+ const modelMap = {
24
+ openai: [
25
+ "gpt-4.1",
26
+ "gpt-4.1-mini",
27
+ "gpt-4o",
28
+ "gpt-4o-mini",
29
+ "gpt-4-turbo",
30
+ "gpt-4",
31
+ "gpt-3.5-turbo"
32
+ ],
33
+ claude: [
34
+ "claude-3-5-sonnet-20241022",
35
+ "claude-3-5-haiku-20241022",
36
+ "claude-3-opus-20240229",
37
+ "claude-3-sonnet-20240229",
38
+ "claude-3-haiku-20240307"
39
+ ],
40
+ gemini: [
41
+ "gemini-2.0-flash-exp",
42
+ "gemini-2.0-flash-thinking-exp-01-21",
43
+ "gemini-exp-1206",
44
+ "gemini-1.5-pro",
45
+ "gemini-1.5-flash",
46
+ "gemini-1.5-flash-8b"
47
+ ],
48
+ groq: [
49
+ "llama-3.3-70b-versatile",
50
+ "llama-3.1-8b-instant",
51
+ "gemma2-9b-it"
52
+ ],
53
+ deepseek: [
54
+ "deepseek-chat",
55
+ "deepseek-reasoner"
56
+ ],
57
+ xai: [
58
+ "grok-4-0131",
59
+ "grok-beta",
60
+ "grok-2-1212"
61
+ ],
62
+ mistral: [
63
+ "mistral-large-latest",
64
+ "mistral-small-latest",
65
+ "pixtral-large-latest",
66
+ "ministral-8b-latest",
67
+ "mistral-nemo"
68
+ ],
69
+ perplexity: [
70
+ "llama-3.1-sonar-large-128k-online",
71
+ "llama-3.1-sonar-small-128k-online",
72
+ "llama-3.1-sonar-large-128k-chat",
73
+ "llama-3.1-sonar-small-128k-chat"
74
+ ],
75
+ openrouter: [
76
+ "openai/gpt-4o",
77
+ "anthropic/claude-3.5-sonnet",
78
+ "google/gemini-2.0-flash-exp",
79
+ "meta-llama/llama-3.3-70b-instruct",
80
+ "deepseek/deepseek-chat"
81
+ ]
82
+ };
83
+ return modelMap[provider] || [];
84
+ }
85
+ // Get all supported providers
86
+ function getAllProviders() {
87
+ return Object.keys(exports.PROVIDER_NAMES);
88
+ }
89
+ function getProviderInfo(provider) {
90
+ return {
91
+ key: provider,
92
+ name: exports.PROVIDER_NAMES[provider],
93
+ models: getProviderModels(provider)
94
+ };
95
+ }
96
+ function getAllProviderInfo() {
97
+ return getAllProviders().map(getProviderInfo);
98
+ }
package/dist/cjs/wrap.js CHANGED
@@ -34,16 +34,43 @@ function wrap(fn, options) {
34
34
  }
35
35
  return async (...args) => {
36
36
  try {
37
- const input = fn(...args);
37
+ // Await the function result to handle both sync and async functions
38
+ const rawInput = fn(...args);
39
+ const input = rawInput instanceof Promise ? await rawInput : rawInput;
40
+ // Check if input is multimodal
41
+ const isMultimodal = typeof input === 'object' && input !== null && ('image' in input || 'file' in input);
42
+ const textInput = isMultimodal ? input.text || '' : String(input);
43
+ const imageData = isMultimodal ? input.image : undefined;
44
+ const fileData = isMultimodal ? input.file : undefined;
38
45
  // Step 1: get provider function and the actual provider name
39
46
  const { fn: providerFn, provider: providerKey } = (0, providers_1.getProvider)(options.provider);
40
47
  // Step 2: pick model: passed model or provider-specific default
41
48
  const model = options.model || (providerKey in types_1.DEFAULT_MODEL ? types_1.DEFAULT_MODEL[providerKey] : undefined);
42
49
  if (!model) {
43
- 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 - AI_HOOK_OPENAI_KEY\n - AI_HOOK_OPENROUTER_KEY\n - AI_HOOK_GROQ_KEY\n", options.provider, "Reference .env.example for setup instructions.");
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.");
51
+ }
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}]`;
62
+ }
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]`;
69
+ }
70
+ if (fileData) {
71
+ prompt = `${prompt}\n\n[File: ${fileData.name}]`;
72
+ }
44
73
  }
45
- // Step 3: build prompt
46
- const prompt = buildPrompt(options.task, input, options.targetLanguage);
47
74
  const startTime = Date.now();
48
75
  let output;
49
76
  try {
package/dist/esm/index.js CHANGED
@@ -1,3 +1,7 @@
1
1
  // src/index.ts
2
2
  export { wrap } from "./wrap";
3
3
  export { initAIHooks, addProvider, removeProvider, getAvailableProviders, getProvider, isInitialized, reset } from "./providers";
4
+ // Export provider utilities
5
+ export { PROVIDER_NAMES, getProviderModels, getAllProviders, getProviderInfo, getAllProviderInfo } from "./utils/providerInfo";
6
+ // Export key validation utilities
7
+ export { validateApiKey, validateApiKeys, quickValidateKeyFormat } from "./utils/keyValidator";
@@ -26,7 +26,7 @@ export const providerConfigs = {
26
26
  openai: {
27
27
  name: "openai",
28
28
  baseUrl: "https://api.openai.com/v1/chat/completions",
29
- envKey: "AI_HOOK_OPENAI_KEY",
29
+ envKey: "OPENAI_KEY",
30
30
  headers: { "Content-Type": "application/json" },
31
31
  requestBody: requestBodyBuilders.openaiStyle,
32
32
  responseParser: responseParsers.openaiStyle,
@@ -43,7 +43,7 @@ export const providerConfigs = {
43
43
  groq: {
44
44
  name: "groq",
45
45
  baseUrl: "https://api.groq.com/openai/v1/chat/completions",
46
- envKey: "AI_HOOK_GROQ_KEY",
46
+ envKey: "GROQ_KEY",
47
47
  headers: { "Content-Type": "application/json" },
48
48
  requestBody: requestBodyBuilders.openaiStyle,
49
49
  responseParser: responseParsers.openaiStyle,
@@ -60,7 +60,7 @@ export const providerConfigs = {
60
60
  claude: {
61
61
  name: "claude",
62
62
  baseUrl: "https://api.anthropic.com/v1/messages",
63
- envKey: "AI_HOOK_CLAUDE_KEY",
63
+ envKey: "CLAUDE_KEY",
64
64
  headers: {
65
65
  "Content-Type": "application/json",
66
66
  "anthropic-version": "2023-06-01"
@@ -80,7 +80,7 @@ export const providerConfigs = {
80
80
  gemini: {
81
81
  name: "gemini",
82
82
  baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
83
- envKey: "AI_HOOK_GEMINI_KEY",
83
+ envKey: "GEMINI_KEY",
84
84
  headers: { "Content-Type": "application/json" },
85
85
  requestBody: requestBodyBuilders.geminiStyle,
86
86
  responseParser: responseParsers.geminiStyle,
@@ -97,7 +97,7 @@ export const providerConfigs = {
97
97
  deepseek: {
98
98
  name: "deepseek",
99
99
  baseUrl: "https://api.deepseek.com/v1/chat/completions",
100
- envKey: "AI_HOOK_DEEPSEEK_KEY",
100
+ envKey: "DEEPSEEK_KEY",
101
101
  headers: { "Content-Type": "application/json" },
102
102
  requestBody: requestBodyBuilders.openaiStyle,
103
103
  responseParser: responseParsers.openaiStyle,
@@ -114,7 +114,7 @@ export const providerConfigs = {
114
114
  mistral: {
115
115
  name: "mistral",
116
116
  baseUrl: "https://api.mistral.ai/v1/chat/completions",
117
- envKey: "AI_HOOK_MISTRAL_KEY",
117
+ envKey: "MISTRAL_KEY",
118
118
  headers: { "Content-Type": "application/json" },
119
119
  requestBody: requestBodyBuilders.openaiStyle,
120
120
  responseParser: responseParsers.openaiStyle,
@@ -131,7 +131,7 @@ export const providerConfigs = {
131
131
  xai: {
132
132
  name: "xai",
133
133
  baseUrl: "https://api.x.ai/v1/chat/completions",
134
- envKey: "AI_HOOK_XAI_KEY",
134
+ envKey: "XAI_KEY",
135
135
  headers: { "Content-Type": "application/json" },
136
136
  requestBody: requestBodyBuilders.openaiStyle,
137
137
  responseParser: responseParsers.openaiStyle,
@@ -148,7 +148,7 @@ export const providerConfigs = {
148
148
  perplexity: {
149
149
  name: "perplexity",
150
150
  baseUrl: "https://api.perplexity.ai/chat/completions",
151
- envKey: "AI_HOOK_PERPLEXITY_KEY",
151
+ envKey: "PERPLEXITY_KEY",
152
152
  headers: { "Content-Type": "application/json" },
153
153
  requestBody: requestBodyBuilders.openaiStyle,
154
154
  responseParser: responseParsers.openaiStyle,
@@ -165,7 +165,7 @@ export const providerConfigs = {
165
165
  openrouter: {
166
166
  name: "openrouter",
167
167
  baseUrl: "https://openrouter.ai/api/v1/chat/completions",
168
- envKey: "AI_HOOK_OPENROUTER_KEY",
168
+ envKey: "OPENROUTER_KEY",
169
169
  headers: { "Content-Type": "application/json" },
170
170
  requestBody: requestBodyBuilders.openaiStyle,
171
171
  responseParser: responseParsers.openaiStyle,
@@ -67,7 +67,7 @@ export function getProvider(name) {
67
67
  return { fn: providers[available[0]], provider: available[0] };
68
68
  }
69
69
  // 3. No valid keys found → throw error (single instruction, no fallback)
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 - AI_HOOK_OPENAI_KEY\n - AI_HOOK_OPENROUTER_KEY\n - AI_HOOK_GROQ_KEY\n", undefined, "Reference .env.example for setup instructions.");
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
72
  // Export the new initialization system
73
73
  export { initAIHooks, addProvider, removeProvider, isInitialized, reset };
@@ -0,0 +1,154 @@
1
+ import { BaseProvider } from "../providers/base/BaseProvider";
2
+ import { ClaudeProvider, GeminiProvider, OpenRouterProvider } from "../providers/base/SpecializedProviders";
3
+ import { providerConfigs } from "../providers/base/ProviderConfigs";
4
+ /**
5
+ * Validates an API key by making a minimal test request to the provider
6
+ */
7
+ export async function validateApiKey(provider, apiKey) {
8
+ if (!apiKey || apiKey.trim() === '') {
9
+ return {
10
+ valid: false,
11
+ provider,
12
+ error: 'EMPTY_KEY',
13
+ message: 'API key is empty'
14
+ };
15
+ }
16
+ try {
17
+ let providerInstance;
18
+ // Create provider instance based on type
19
+ switch (provider) {
20
+ case 'claude':
21
+ providerInstance = new ClaudeProvider();
22
+ break;
23
+ case 'gemini':
24
+ providerInstance = new GeminiProvider();
25
+ break;
26
+ case 'openrouter':
27
+ providerInstance = new OpenRouterProvider();
28
+ break;
29
+ default:
30
+ const config = providerConfigs[provider];
31
+ if (!config) {
32
+ return {
33
+ valid: false,
34
+ provider,
35
+ error: 'UNKNOWN_PROVIDER',
36
+ message: `Unknown provider: ${provider}`
37
+ };
38
+ }
39
+ providerInstance = new BaseProvider(config);
40
+ }
41
+ // Set the API key temporarily
42
+ providerInstance.apiKey = apiKey;
43
+ // Make a minimal test request
44
+ const testPrompt = "Hi";
45
+ const testModel = getDefaultModel(provider);
46
+ await providerInstance.call(testPrompt, testModel);
47
+ return {
48
+ valid: true,
49
+ provider,
50
+ message: 'API key is valid'
51
+ };
52
+ }
53
+ catch (error) {
54
+ // Check for common error types
55
+ if (error.message?.includes('401') || error.message?.includes('Unauthorized') || error.message?.includes('Invalid API key')) {
56
+ return {
57
+ valid: false,
58
+ provider,
59
+ error: 'INVALID_KEY',
60
+ message: 'API key is invalid or unauthorized'
61
+ };
62
+ }
63
+ if (error.message?.includes('403') || error.message?.includes('Forbidden')) {
64
+ return {
65
+ valid: false,
66
+ provider,
67
+ error: 'FORBIDDEN',
68
+ message: 'API key does not have access to this resource'
69
+ };
70
+ }
71
+ if (error.message?.includes('429') || error.message?.includes('rate limit')) {
72
+ // Rate limit means the key is valid, just too many requests
73
+ return {
74
+ valid: true,
75
+ provider,
76
+ message: 'API key is valid (rate limited)'
77
+ };
78
+ }
79
+ // If we get other errors, the key might still be valid (could be model/format issues)
80
+ return {
81
+ valid: true, // Assume valid unless we get clear auth error
82
+ provider,
83
+ message: 'API key appears valid (test request had non-auth error)'
84
+ };
85
+ }
86
+ }
87
+ /**
88
+ * Validates multiple API keys
89
+ */
90
+ export async function validateApiKeys(keys) {
91
+ const results = {};
92
+ for (const [provider, key] of Object.entries(keys)) {
93
+ if (key && key.trim()) {
94
+ results[provider] = await validateApiKey(provider, key);
95
+ }
96
+ }
97
+ return results;
98
+ }
99
+ /**
100
+ * Get default model for a provider (for testing)
101
+ */
102
+ function getDefaultModel(provider) {
103
+ const defaultModels = {
104
+ openai: 'gpt-3.5-turbo',
105
+ claude: 'claude-3-haiku-20240307',
106
+ gemini: 'gemini-1.5-flash',
107
+ groq: 'llama-3.1-8b-instant',
108
+ deepseek: 'deepseek-chat',
109
+ xai: 'grok-beta',
110
+ mistral: 'mistral-small-latest',
111
+ perplexity: 'llama-3.1-sonar-small-128k-chat',
112
+ openrouter: 'openai/gpt-3.5-turbo'
113
+ };
114
+ return defaultModels[provider] || 'default';
115
+ }
116
+ /**
117
+ * Quick check if API key format looks valid (doesn't make API call)
118
+ */
119
+ export function quickValidateKeyFormat(provider, apiKey) {
120
+ if (!apiKey || apiKey.trim() === '') {
121
+ return {
122
+ valid: false,
123
+ provider,
124
+ error: 'EMPTY_KEY',
125
+ message: 'API key is empty'
126
+ };
127
+ }
128
+ // Check key format patterns
129
+ const patterns = {
130
+ openai: /^sk-[a-zA-Z0-9-_]{20,}$/,
131
+ claude: /^sk-ant-[a-zA-Z0-9-_]{20,}$/,
132
+ gemini: /^AIza[a-zA-Z0-9-_]{35}$/,
133
+ groq: /^gsk_[a-zA-Z0-9]{52}$/,
134
+ deepseek: /^sk-[a-zA-Z0-9]{32}$/,
135
+ xai: /^xai-[a-zA-Z0-9-_]{40,}$/,
136
+ mistral: /^[a-zA-Z0-9]{32}$/,
137
+ perplexity: /^pplx-[a-zA-Z0-9]{40}$/,
138
+ openrouter: /^sk-or-v1-[a-zA-Z0-9]{64}$/
139
+ };
140
+ const pattern = patterns[provider];
141
+ if (pattern && !pattern.test(apiKey)) {
142
+ return {
143
+ valid: false,
144
+ provider,
145
+ error: 'INVALID_FORMAT',
146
+ message: `API key format doesn't match expected pattern for ${provider}`
147
+ };
148
+ }
149
+ return {
150
+ valid: true,
151
+ provider,
152
+ message: 'API key format looks valid'
153
+ };
154
+ }