npm-ai-hooks 1.0.2 → 2.0.0
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 +1 -1
- package/Readme.md +379 -137
- package/dist/cjs/index.js +14 -0
- package/dist/cjs/providers/base/BaseProvider.js +121 -0
- package/dist/cjs/providers/base/ProviderConfig.js +118 -0
- package/dist/cjs/providers/base/ProviderConfigs.js +185 -0
- package/dist/cjs/providers/base/ProviderRegistry.js +47 -0
- package/dist/cjs/providers/base/SpecializedProviders.js +58 -0
- package/dist/cjs/providers/index.js +82 -0
- package/dist/cjs/providers/init.js +90 -0
- package/dist/{wrap.js → cjs/wrap.js} +43 -12
- package/dist/esm/errors.js +17 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/providers/base/BaseProvider.js +114 -0
- package/dist/esm/providers/base/ProviderConfig.js +114 -0
- package/dist/esm/providers/base/ProviderConfigs.js +182 -0
- package/dist/esm/providers/base/ProviderRegistry.js +43 -0
- package/dist/esm/providers/base/SpecializedProviders.js +52 -0
- package/dist/esm/providers/index.js +75 -0
- package/dist/esm/providers/init.js +81 -0
- package/dist/esm/types/claude.js +4 -0
- package/dist/esm/types/core/providers.js +1 -0
- package/dist/esm/types/deepseek.js +4 -0
- package/dist/esm/types/gemini.js +4 -0
- package/dist/esm/types/groq.js +1 -0
- package/dist/esm/types/index.js +20 -0
- package/dist/esm/types/mistral.js +4 -0
- package/dist/esm/types/openai.js +4 -0
- package/dist/esm/types/openrouter.js +1 -0
- package/dist/esm/types/perplexity.js +4 -0
- package/dist/esm/types/xai.js +4 -0
- package/dist/esm/wrap.js +113 -0
- package/dist/index.d.ts +1 -0
- package/dist/providers/base/BaseProvider.d.ts +33 -0
- package/dist/providers/base/ProviderConfig.d.ts +28 -0
- package/dist/providers/base/ProviderConfigs.d.ts +31 -0
- package/dist/providers/base/ProviderRegistry.d.ts +12 -0
- package/dist/providers/base/SpecializedProviders.d.ts +68 -0
- package/dist/providers/index.d.ts +4 -0
- package/dist/providers/init.d.ts +58 -0
- package/package.json +50 -12
- package/dist/index.js +0 -6
- package/dist/providers/claude.d.ts +0 -2
- package/dist/providers/claude.js +0 -60
- package/dist/providers/deepkseek.d.ts +0 -2
- package/dist/providers/deepkseek.js +0 -57
- package/dist/providers/gemini.d.ts +0 -2
- package/dist/providers/gemini.js +0 -58
- package/dist/providers/groq.d.ts +0 -2
- package/dist/providers/groq.js +0 -57
- package/dist/providers/index.js +0 -75
- package/dist/providers/mistral.d.ts +0 -2
- package/dist/providers/mistral.js +0 -57
- package/dist/providers/openai.d.ts +0 -2
- package/dist/providers/openai.js +0 -57
- package/dist/providers/openrouter.d.ts +0 -1
- package/dist/providers/openrouter.js +0 -55
- package/dist/providers/perplexity.d.ts +0 -2
- package/dist/providers/perplexity.js +0 -57
- package/dist/providers/xai.d.ts +0 -2
- package/dist/providers/xai.js +0 -57
- /package/dist/{errors.js → cjs/errors.js} +0 -0
- /package/dist/{types → cjs/types}/claude.js +0 -0
- /package/dist/{types → cjs/types}/core/providers.js +0 -0
- /package/dist/{types → cjs/types}/deepseek.js +0 -0
- /package/dist/{types → cjs/types}/gemini.js +0 -0
- /package/dist/{types → cjs/types}/groq.js +0 -0
- /package/dist/{types → cjs/types}/index.js +0 -0
- /package/dist/{types → cjs/types}/mistral.js +0 -0
- /package/dist/{types → cjs/types}/openai.js +0 -0
- /package/dist/{types → cjs/types}/openrouter.js +0 -0
- /package/dist/{types → cjs/types}/perplexity.js +0 -0
- /package/dist/{types → cjs/types}/xai.js +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initAIHooks = initAIHooks;
|
|
4
|
+
exports.addProvider = addProvider;
|
|
5
|
+
exports.removeProvider = removeProvider;
|
|
6
|
+
exports.getAvailableProviders = getAvailableProviders;
|
|
7
|
+
exports.getProvider = getProvider;
|
|
8
|
+
exports.isInitialized = isInitialized;
|
|
9
|
+
exports.reset = reset;
|
|
10
|
+
const ProviderConfig_1 = require("./base/ProviderConfig");
|
|
11
|
+
// Global provider manager instance
|
|
12
|
+
let providerManager = null;
|
|
13
|
+
/**
|
|
14
|
+
* Initialize the AI hooks system with provider configurations
|
|
15
|
+
* @param options - Provider initialization options
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { initAIHooks } from 'npm-ai-hooks';
|
|
19
|
+
*
|
|
20
|
+
* initAIHooks({
|
|
21
|
+
* providers: [
|
|
22
|
+
* { provider: 'openai', key: 'sk-...', defaultModel: 'gpt-4' },
|
|
23
|
+
* { provider: 'claude', key: 'sk-ant-...', defaultModel: 'claude-3-sonnet-20240229' },
|
|
24
|
+
* { provider: 'groq', key: 'gsk_...', defaultModel: 'llama-3.1-70b-versatile' }
|
|
25
|
+
* ],
|
|
26
|
+
* defaultProvider: 'openai' // optional
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
function initAIHooks(options) {
|
|
31
|
+
providerManager = new ProviderConfig_1.ProviderManager(options);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Add a new provider after initialization
|
|
35
|
+
* @param config - Provider configuration
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* addProvider({ provider: 'mistral', key: '...', defaultModel: 'mistral-large' });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
function addProvider(config) {
|
|
42
|
+
if (!providerManager) {
|
|
43
|
+
throw new Error('AI hooks not initialized. Call initAIHooks() first.');
|
|
44
|
+
}
|
|
45
|
+
providerManager.addProvider(config);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Remove a provider
|
|
49
|
+
* @param provider - Provider to remove
|
|
50
|
+
*/
|
|
51
|
+
function removeProvider(provider) {
|
|
52
|
+
if (!providerManager) {
|
|
53
|
+
throw new Error('AI hooks not initialized. Call initAIHooks() first.');
|
|
54
|
+
}
|
|
55
|
+
providerManager.removeProvider(provider);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get available providers
|
|
59
|
+
* @returns Array of available provider names
|
|
60
|
+
*/
|
|
61
|
+
function getAvailableProviders() {
|
|
62
|
+
if (!providerManager) {
|
|
63
|
+
throw new Error('AI hooks not initialized. Call initAIHooks() first.');
|
|
64
|
+
}
|
|
65
|
+
return providerManager.getAvailableProviders();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get a provider function
|
|
69
|
+
* @param name - Optional provider name
|
|
70
|
+
* @returns Provider function and name
|
|
71
|
+
*/
|
|
72
|
+
function getProvider(name) {
|
|
73
|
+
if (!providerManager) {
|
|
74
|
+
throw new Error('AI hooks not initialized. Call initAIHooks() first.');
|
|
75
|
+
}
|
|
76
|
+
return providerManager.getProvider(name);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if AI hooks is initialized
|
|
80
|
+
* @returns True if initialized
|
|
81
|
+
*/
|
|
82
|
+
function isInitialized() {
|
|
83
|
+
return providerManager !== null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Reset the provider manager (useful for testing)
|
|
87
|
+
*/
|
|
88
|
+
function reset() {
|
|
89
|
+
providerManager = null;
|
|
90
|
+
}
|
|
@@ -1,26 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.wrap = wrap;
|
|
7
|
-
|
|
8
|
-
dotenv_1.default.config();
|
|
4
|
+
// dotenv removed - using explicit provider initialization instead
|
|
9
5
|
const providers_1 = require("./providers");
|
|
10
6
|
const types_1 = require("./types");
|
|
7
|
+
const errors_1 = require("./errors");
|
|
11
8
|
function handleError(err) {
|
|
12
9
|
if (err && typeof err === "object" && "pretty" in err && typeof err.pretty === "function") {
|
|
13
|
-
// Print pretty message
|
|
10
|
+
// Print pretty message
|
|
14
11
|
console.error(err.pretty());
|
|
15
|
-
|
|
12
|
+
// Only exit in Node.js test environment, not in browser
|
|
13
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test" && !process.env.JEST_WORKER_ID) {
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
// In browser or test mode, just throw the error instead of exiting
|
|
17
|
+
throw err;
|
|
16
18
|
}
|
|
17
19
|
else {
|
|
18
20
|
console.error(err);
|
|
19
|
-
|
|
21
|
+
// Only exit in Node.js test environment, not in browser
|
|
22
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test" && !process.env.JEST_WORKER_ID) {
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
// In browser or test mode, just throw the error instead of exiting
|
|
26
|
+
throw err;
|
|
20
27
|
}
|
|
21
|
-
throw new Error("Process exited due to AIHookError"); // for TS never
|
|
22
28
|
}
|
|
23
29
|
function wrap(fn, options) {
|
|
30
|
+
// Validate task type immediately when wrap() is called
|
|
31
|
+
const validTasks = ["summarize", "translate", "explain", "rewrite", "sentiment", "codeReview"];
|
|
32
|
+
if (options.task && !validTasks.includes(options.task)) {
|
|
33
|
+
throw new errors_1.AIHookError("INVALID_TASK", `Invalid task type: ${options.task}. Valid tasks are: ${validTasks.join(", ")}`, options.provider, "Please use one of the supported task types.");
|
|
34
|
+
}
|
|
24
35
|
return async (...args) => {
|
|
25
36
|
try {
|
|
26
37
|
const input = fn(...args);
|
|
@@ -29,7 +40,7 @@ function wrap(fn, options) {
|
|
|
29
40
|
// Step 2: pick model: passed model or provider-specific default
|
|
30
41
|
const model = options.model || (providerKey in types_1.DEFAULT_MODEL ? types_1.DEFAULT_MODEL[providerKey] : undefined);
|
|
31
42
|
if (!model) {
|
|
32
|
-
throw new
|
|
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.");
|
|
33
44
|
}
|
|
34
45
|
// Step 3: build prompt
|
|
35
46
|
const prompt = buildPrompt(options.task, input, options.targetLanguage);
|
|
@@ -39,8 +50,9 @@ function wrap(fn, options) {
|
|
|
39
50
|
output = await providerFn(prompt, model);
|
|
40
51
|
}
|
|
41
52
|
catch (err) {
|
|
42
|
-
if (err instanceof
|
|
43
|
-
|
|
53
|
+
if (err instanceof errors_1.AIHookError) {
|
|
54
|
+
// For AIHookError, just re-throw it - it will be handled by the outer catch
|
|
55
|
+
throw err;
|
|
44
56
|
}
|
|
45
57
|
if (err instanceof Error) {
|
|
46
58
|
throw new Error(`[ai-hooks] Unknown error calling provider: ${err.message}`);
|
|
@@ -60,11 +72,30 @@ function wrap(fn, options) {
|
|
|
60
72
|
};
|
|
61
73
|
}
|
|
62
74
|
catch (err) {
|
|
75
|
+
if (err instanceof errors_1.AIHookError) {
|
|
76
|
+
// For AIHookError, just log the pretty message without the full error handling
|
|
77
|
+
console.error(err.pretty());
|
|
78
|
+
// Return a mock response to prevent the demo from crashing
|
|
79
|
+
return {
|
|
80
|
+
output: "Error occurred",
|
|
81
|
+
meta: {
|
|
82
|
+
provider: "unknown",
|
|
83
|
+
model: "unknown",
|
|
84
|
+
cached: false,
|
|
85
|
+
estimatedCostUSD: 0.0,
|
|
86
|
+
latencyMs: 0,
|
|
87
|
+
error: true
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
63
91
|
handleError(err);
|
|
64
92
|
}
|
|
65
93
|
};
|
|
66
94
|
}
|
|
67
95
|
function buildPrompt(task, text, targetLanguage) {
|
|
96
|
+
if (!task) {
|
|
97
|
+
return text; // If no task specified, just return the text as-is
|
|
98
|
+
}
|
|
68
99
|
switch (task) {
|
|
69
100
|
case "summarize":
|
|
70
101
|
return `Summarize the following text:\n${text}`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export class AIHookError extends Error {
|
|
2
|
+
constructor(code, message, provider, suggestion) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.code = code;
|
|
5
|
+
this.provider = provider;
|
|
6
|
+
this.suggestion = suggestion;
|
|
7
|
+
// Do NOT print pretty message in constructor
|
|
8
|
+
}
|
|
9
|
+
pretty() {
|
|
10
|
+
return `\n❌ AI-HOOK ERROR: ${this.message}` +
|
|
11
|
+
(this.provider ? `\n Provider: ${this.provider}` : "") +
|
|
12
|
+
(this.suggestion ? `\n Suggestion: ${this.suggestion}\n` : "");
|
|
13
|
+
}
|
|
14
|
+
toString() {
|
|
15
|
+
return this.pretty();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { AIHookError } from "../../errors";
|
|
3
|
+
export class BaseProvider {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
}
|
|
7
|
+
async call(prompt, model) {
|
|
8
|
+
const apiKey = this.getApiKey();
|
|
9
|
+
this.validateApiKey(apiKey);
|
|
10
|
+
// At this point, apiKey is guaranteed to be defined due to validateApiKey
|
|
11
|
+
const validatedApiKey = apiKey;
|
|
12
|
+
try {
|
|
13
|
+
const requestConfig = this.buildRequestConfig(prompt, model, validatedApiKey);
|
|
14
|
+
const response = await this.makeRequest(requestConfig);
|
|
15
|
+
return this.parseResponse(response);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
throw this.handleError(error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
getApiKey() {
|
|
22
|
+
// This method should be overridden by the provider creation logic
|
|
23
|
+
// to return the explicitly provided API key instead of reading from process.env
|
|
24
|
+
if (typeof process !== "undefined" && process.env) {
|
|
25
|
+
return process.env[this.config.envKey];
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
validateApiKey(apiKey) {
|
|
30
|
+
if (!apiKey) {
|
|
31
|
+
throw new AIHookError("INVALID_API_KEY", this.config.errorMessages.missingKey, this.config.name, `Set ${this.config.envKey} in your environment variables.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
buildRequestConfig(prompt, model, apiKey) {
|
|
35
|
+
return {
|
|
36
|
+
url: this.config.baseUrl,
|
|
37
|
+
method: "POST",
|
|
38
|
+
data: this.config.requestBody(prompt, model),
|
|
39
|
+
headers: {
|
|
40
|
+
...this.config.headers,
|
|
41
|
+
...this.buildAuthHeaders(apiKey)
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
buildAuthHeaders(apiKey) {
|
|
46
|
+
return {
|
|
47
|
+
"Authorization": `Bearer ${apiKey}`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async makeRequest(config) {
|
|
51
|
+
return axios(config);
|
|
52
|
+
}
|
|
53
|
+
parseResponse(response) {
|
|
54
|
+
const output = this.config.responseParser(response);
|
|
55
|
+
if (!output) {
|
|
56
|
+
throw new AIHookError("PROVIDER_ERROR", this.config.errorMessages.emptyResponse, this.config.name, "Check your model and API key");
|
|
57
|
+
}
|
|
58
|
+
return output;
|
|
59
|
+
}
|
|
60
|
+
handleError(error) {
|
|
61
|
+
if (error.response) {
|
|
62
|
+
return this.handleHttpError(error);
|
|
63
|
+
}
|
|
64
|
+
else if (error.request) {
|
|
65
|
+
return new AIHookError("NETWORK_ERROR", this.config.errorMessages.networkError, this.config.name, "Check your internet connection");
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
return new AIHookError("UNKNOWN_ERROR", error.message || this.config.errorMessages.unknownError, this.config.name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
getCapitalizedProviderName() {
|
|
72
|
+
const name = this.config.name;
|
|
73
|
+
// Handle special cases
|
|
74
|
+
if (name === "openai")
|
|
75
|
+
return "OpenAI";
|
|
76
|
+
if (name === "openrouter")
|
|
77
|
+
return "OpenRouter";
|
|
78
|
+
if (name === "xai")
|
|
79
|
+
return "xAI";
|
|
80
|
+
if (name === "claude")
|
|
81
|
+
return "Claude";
|
|
82
|
+
if (name === "gemini")
|
|
83
|
+
return "Gemini";
|
|
84
|
+
if (name === "groq")
|
|
85
|
+
return "Groq";
|
|
86
|
+
if (name === "deepseek")
|
|
87
|
+
return "DeepSeek";
|
|
88
|
+
if (name === "mistral")
|
|
89
|
+
return "Mistral";
|
|
90
|
+
if (name === "perplexity")
|
|
91
|
+
return "Perplexity";
|
|
92
|
+
// Default: capitalize first letter
|
|
93
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
94
|
+
}
|
|
95
|
+
handleHttpError(error) {
|
|
96
|
+
const status = error.response.status;
|
|
97
|
+
const text = error.response.data?.error
|
|
98
|
+
? JSON.stringify(error.response.data.error)
|
|
99
|
+
: error.response.statusText || "Unknown error";
|
|
100
|
+
const providerName = this.config.name;
|
|
101
|
+
switch (status) {
|
|
102
|
+
case 400:
|
|
103
|
+
return new AIHookError("BAD_REQUEST", `${this.getCapitalizedProviderName()} rejected the request: ${text}`, providerName, "Check your prompt and model");
|
|
104
|
+
case 401:
|
|
105
|
+
return new AIHookError("INVALID_API_KEY", `Invalid ${this.getCapitalizedProviderName()} API key: ${text}`, providerName, `Verify your ${this.config.envKey} environment variable`);
|
|
106
|
+
case 403:
|
|
107
|
+
return new AIHookError("MODEL_NOT_ALLOWED", `Your API key cannot access this model: ${text}`, providerName, "Try a different model or check API key permissions");
|
|
108
|
+
case 429:
|
|
109
|
+
return new AIHookError("RATE_LIMIT", `Too many requests to ${this.getCapitalizedProviderName()}: ${text}`, providerName, "Throttle requests or upgrade your plan");
|
|
110
|
+
default:
|
|
111
|
+
return new AIHookError("PROVIDER_ERROR", `${this.getCapitalizedProviderName()} API error: ${text}`, providerName);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { DEFAULT_MODEL } from "../../types";
|
|
2
|
+
import { BaseProvider } from "./BaseProvider";
|
|
3
|
+
import { providerConfigs } from "./ProviderConfigs";
|
|
4
|
+
import { ClaudeProvider, GeminiProvider, OpenRouterProvider } from "./SpecializedProviders";
|
|
5
|
+
export class ProviderManager {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.providers = new Map();
|
|
8
|
+
this.providerFunctions = new Map();
|
|
9
|
+
this.initializeProviders(options);
|
|
10
|
+
}
|
|
11
|
+
initializeProviders(options) {
|
|
12
|
+
// Store provider configs
|
|
13
|
+
for (const config of options.providers) {
|
|
14
|
+
this.providers.set(config.provider, {
|
|
15
|
+
key: config.key,
|
|
16
|
+
defaultModel: config.defaultModel
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
// Set default provider if specified
|
|
20
|
+
this.defaultProvider = options.defaultProvider;
|
|
21
|
+
// Create provider functions
|
|
22
|
+
this.createProviderFunctions();
|
|
23
|
+
}
|
|
24
|
+
createProviderFunctions() {
|
|
25
|
+
for (const [providerName, config] of this.providers) {
|
|
26
|
+
const providerFunction = this.createProviderFunction(providerName, config);
|
|
27
|
+
this.providerFunctions.set(providerName, providerFunction);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
createProviderFunction(providerName, config) {
|
|
31
|
+
return async (prompt, model) => {
|
|
32
|
+
// Use provided model or default model for this provider
|
|
33
|
+
const modelToUse = model || config.defaultModel || this.getDefaultModelForProvider(providerName);
|
|
34
|
+
// Create a temporary provider instance for this call
|
|
35
|
+
const provider = this.createProviderInstance(providerName, config.key);
|
|
36
|
+
return provider.call(prompt, modelToUse);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
createProviderInstance(providerName, apiKey) {
|
|
40
|
+
// Import the base provider and configs
|
|
41
|
+
// Using imported classes directly
|
|
42
|
+
const config = providerConfigs[providerName];
|
|
43
|
+
if (!config) {
|
|
44
|
+
throw new Error(`Unsupported provider: ${providerName}`);
|
|
45
|
+
}
|
|
46
|
+
// Create provider instance with the provided API key
|
|
47
|
+
const providerConfig = { ...config };
|
|
48
|
+
// Override the getApiKey method to return the provided key
|
|
49
|
+
if (providerName === 'claude') {
|
|
50
|
+
const provider = new ClaudeProvider();
|
|
51
|
+
provider['getApiKey'] = () => apiKey;
|
|
52
|
+
return provider;
|
|
53
|
+
}
|
|
54
|
+
else if (providerName === 'gemini') {
|
|
55
|
+
const provider = new GeminiProvider();
|
|
56
|
+
provider['getApiKey'] = () => apiKey;
|
|
57
|
+
return provider;
|
|
58
|
+
}
|
|
59
|
+
else if (providerName === 'openrouter') {
|
|
60
|
+
const provider = new OpenRouterProvider();
|
|
61
|
+
provider['getApiKey'] = () => apiKey;
|
|
62
|
+
return provider;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const provider = new BaseProvider(providerConfig);
|
|
66
|
+
provider['getApiKey'] = () => apiKey;
|
|
67
|
+
return provider;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
getDefaultModelForProvider(providerName) {
|
|
71
|
+
// Import default models
|
|
72
|
+
// Using imported DEFAULT_MODEL directly
|
|
73
|
+
return DEFAULT_MODEL[providerName];
|
|
74
|
+
}
|
|
75
|
+
getAvailableProviders() {
|
|
76
|
+
return Array.from(this.providers.keys());
|
|
77
|
+
}
|
|
78
|
+
getProvider(name) {
|
|
79
|
+
const available = this.getAvailableProviders();
|
|
80
|
+
if (available.length === 0) {
|
|
81
|
+
throw new Error('No providers initialized. Please initialize providers first.');
|
|
82
|
+
}
|
|
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 };
|
|
90
|
+
}
|
|
91
|
+
// 3. Prefer OpenRouter if available
|
|
92
|
+
if (this.providerFunctions.has('openrouter')) {
|
|
93
|
+
return { fn: this.providerFunctions.get('openrouter'), provider: 'openrouter' };
|
|
94
|
+
}
|
|
95
|
+
// 4. Use first available provider
|
|
96
|
+
const firstProvider = available[0];
|
|
97
|
+
return { fn: this.providerFunctions.get(firstProvider), provider: firstProvider };
|
|
98
|
+
}
|
|
99
|
+
addProvider(config) {
|
|
100
|
+
this.providers.set(config.provider, {
|
|
101
|
+
key: config.key,
|
|
102
|
+
defaultModel: config.defaultModel
|
|
103
|
+
});
|
|
104
|
+
const providerFunction = this.createProviderFunction(config.provider, {
|
|
105
|
+
key: config.key,
|
|
106
|
+
defaultModel: config.defaultModel
|
|
107
|
+
});
|
|
108
|
+
this.providerFunctions.set(config.provider, providerFunction);
|
|
109
|
+
}
|
|
110
|
+
removeProvider(provider) {
|
|
111
|
+
this.providers.delete(provider);
|
|
112
|
+
this.providerFunctions.delete(provider);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Common response parsers
|
|
2
|
+
export const responseParsers = {
|
|
3
|
+
openaiStyle: (response) => response.data?.choices?.[0]?.message?.content,
|
|
4
|
+
claudeStyle: (response) => response.data?.content?.[0]?.text,
|
|
5
|
+
geminiStyle: (response) => response.data?.candidates?.[0]?.content?.parts?.[0]?.text,
|
|
6
|
+
};
|
|
7
|
+
// Common request body builders
|
|
8
|
+
export const requestBodyBuilders = {
|
|
9
|
+
openaiStyle: (prompt, model) => ({
|
|
10
|
+
model,
|
|
11
|
+
messages: [{ role: "user", content: prompt }]
|
|
12
|
+
}),
|
|
13
|
+
claudeStyle: (prompt, model) => ({
|
|
14
|
+
model,
|
|
15
|
+
max_tokens: 4096,
|
|
16
|
+
messages: [{ role: "user", content: prompt }]
|
|
17
|
+
}),
|
|
18
|
+
geminiStyle: (prompt, model) => ({
|
|
19
|
+
contents: [{
|
|
20
|
+
parts: [{ text: prompt }]
|
|
21
|
+
}]
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
// Provider configurations
|
|
25
|
+
export const providerConfigs = {
|
|
26
|
+
openai: {
|
|
27
|
+
name: "openai",
|
|
28
|
+
baseUrl: "https://api.openai.com/v1/chat/completions",
|
|
29
|
+
envKey: "AI_HOOK_OPENAI_KEY",
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
requestBody: requestBodyBuilders.openaiStyle,
|
|
32
|
+
responseParser: responseParsers.openaiStyle,
|
|
33
|
+
errorMessages: {
|
|
34
|
+
missingKey: "Missing OpenAI API key.",
|
|
35
|
+
emptyResponse: "OpenAI returned empty response",
|
|
36
|
+
badRequest: "OpenAI rejected the request",
|
|
37
|
+
invalidKey: "Invalid OpenAI API key",
|
|
38
|
+
rateLimit: "Too many requests to OpenAI",
|
|
39
|
+
networkError: "Network error while contacting OpenAI",
|
|
40
|
+
unknownError: "Unknown error occurred"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
groq: {
|
|
44
|
+
name: "groq",
|
|
45
|
+
baseUrl: "https://api.groq.com/openai/v1/chat/completions",
|
|
46
|
+
envKey: "AI_HOOK_GROQ_KEY",
|
|
47
|
+
headers: { "Content-Type": "application/json" },
|
|
48
|
+
requestBody: requestBodyBuilders.openaiStyle,
|
|
49
|
+
responseParser: responseParsers.openaiStyle,
|
|
50
|
+
errorMessages: {
|
|
51
|
+
missingKey: "Missing Groq API key.",
|
|
52
|
+
emptyResponse: "Groq returned empty response",
|
|
53
|
+
badRequest: "Groq rejected the request",
|
|
54
|
+
invalidKey: "Invalid Groq API key",
|
|
55
|
+
rateLimit: "Too many requests to Groq",
|
|
56
|
+
networkError: "Network error while contacting Groq",
|
|
57
|
+
unknownError: "Unknown error occurred"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
claude: {
|
|
61
|
+
name: "claude",
|
|
62
|
+
baseUrl: "https://api.anthropic.com/v1/messages",
|
|
63
|
+
envKey: "AI_HOOK_CLAUDE_KEY",
|
|
64
|
+
headers: {
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
"anthropic-version": "2023-06-01"
|
|
67
|
+
},
|
|
68
|
+
requestBody: requestBodyBuilders.claudeStyle,
|
|
69
|
+
responseParser: responseParsers.claudeStyle,
|
|
70
|
+
errorMessages: {
|
|
71
|
+
missingKey: "Missing Claude API key.",
|
|
72
|
+
emptyResponse: "Claude returned empty response",
|
|
73
|
+
badRequest: "Claude rejected the request",
|
|
74
|
+
invalidKey: "Invalid Claude API key",
|
|
75
|
+
rateLimit: "Too many requests to Claude",
|
|
76
|
+
networkError: "Network error while contacting Claude",
|
|
77
|
+
unknownError: "Unknown error occurred"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
gemini: {
|
|
81
|
+
name: "gemini",
|
|
82
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
|
|
83
|
+
envKey: "AI_HOOK_GEMINI_KEY",
|
|
84
|
+
headers: { "Content-Type": "application/json" },
|
|
85
|
+
requestBody: requestBodyBuilders.geminiStyle,
|
|
86
|
+
responseParser: responseParsers.geminiStyle,
|
|
87
|
+
errorMessages: {
|
|
88
|
+
missingKey: "Missing Gemini API key.",
|
|
89
|
+
emptyResponse: "Gemini returned empty response",
|
|
90
|
+
badRequest: "Gemini rejected the request",
|
|
91
|
+
invalidKey: "Invalid Gemini API key",
|
|
92
|
+
rateLimit: "Too many requests to Gemini",
|
|
93
|
+
networkError: "Network error while contacting Gemini",
|
|
94
|
+
unknownError: "Unknown error occurred"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
deepseek: {
|
|
98
|
+
name: "deepseek",
|
|
99
|
+
baseUrl: "https://api.deepseek.com/v1/chat/completions",
|
|
100
|
+
envKey: "AI_HOOK_DEEPSEEK_KEY",
|
|
101
|
+
headers: { "Content-Type": "application/json" },
|
|
102
|
+
requestBody: requestBodyBuilders.openaiStyle,
|
|
103
|
+
responseParser: responseParsers.openaiStyle,
|
|
104
|
+
errorMessages: {
|
|
105
|
+
missingKey: "Missing DeepSeek API key.",
|
|
106
|
+
emptyResponse: "DeepSeek returned empty response",
|
|
107
|
+
badRequest: "DeepSeek rejected the request",
|
|
108
|
+
invalidKey: "Invalid DeepSeek API key",
|
|
109
|
+
rateLimit: "Too many requests to DeepSeek",
|
|
110
|
+
networkError: "Network error while contacting DeepSeek",
|
|
111
|
+
unknownError: "Unknown error occurred"
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
mistral: {
|
|
115
|
+
name: "mistral",
|
|
116
|
+
baseUrl: "https://api.mistral.ai/v1/chat/completions",
|
|
117
|
+
envKey: "AI_HOOK_MISTRAL_KEY",
|
|
118
|
+
headers: { "Content-Type": "application/json" },
|
|
119
|
+
requestBody: requestBodyBuilders.openaiStyle,
|
|
120
|
+
responseParser: responseParsers.openaiStyle,
|
|
121
|
+
errorMessages: {
|
|
122
|
+
missingKey: "Missing Mistral API key.",
|
|
123
|
+
emptyResponse: "Mistral returned empty response",
|
|
124
|
+
badRequest: "Mistral rejected the request",
|
|
125
|
+
invalidKey: "Invalid Mistral API key",
|
|
126
|
+
rateLimit: "Too many requests to Mistral",
|
|
127
|
+
networkError: "Network error while contacting Mistral",
|
|
128
|
+
unknownError: "Unknown error occurred"
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
xai: {
|
|
132
|
+
name: "xai",
|
|
133
|
+
baseUrl: "https://api.x.ai/v1/chat/completions",
|
|
134
|
+
envKey: "AI_HOOK_XAI_KEY",
|
|
135
|
+
headers: { "Content-Type": "application/json" },
|
|
136
|
+
requestBody: requestBodyBuilders.openaiStyle,
|
|
137
|
+
responseParser: responseParsers.openaiStyle,
|
|
138
|
+
errorMessages: {
|
|
139
|
+
missingKey: "Missing xAI API key.",
|
|
140
|
+
emptyResponse: "xAI returned empty response",
|
|
141
|
+
badRequest: "xAI rejected the request",
|
|
142
|
+
invalidKey: "Invalid xAI API key",
|
|
143
|
+
rateLimit: "Too many requests to xAI",
|
|
144
|
+
networkError: "Network error while contacting xAI",
|
|
145
|
+
unknownError: "Unknown error occurred"
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
perplexity: {
|
|
149
|
+
name: "perplexity",
|
|
150
|
+
baseUrl: "https://api.perplexity.ai/chat/completions",
|
|
151
|
+
envKey: "AI_HOOK_PERPLEXITY_KEY",
|
|
152
|
+
headers: { "Content-Type": "application/json" },
|
|
153
|
+
requestBody: requestBodyBuilders.openaiStyle,
|
|
154
|
+
responseParser: responseParsers.openaiStyle,
|
|
155
|
+
errorMessages: {
|
|
156
|
+
missingKey: "Missing Perplexity API key.",
|
|
157
|
+
emptyResponse: "Perplexity returned empty response",
|
|
158
|
+
badRequest: "Perplexity rejected the request",
|
|
159
|
+
invalidKey: "Invalid Perplexity API key",
|
|
160
|
+
rateLimit: "Too many requests to Perplexity",
|
|
161
|
+
networkError: "Network error while contacting Perplexity",
|
|
162
|
+
unknownError: "Unknown error occurred"
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
openrouter: {
|
|
166
|
+
name: "openrouter",
|
|
167
|
+
baseUrl: "https://openrouter.ai/api/v1/chat/completions",
|
|
168
|
+
envKey: "AI_HOOK_OPENROUTER_KEY",
|
|
169
|
+
headers: { "Content-Type": "application/json" },
|
|
170
|
+
requestBody: requestBodyBuilders.openaiStyle,
|
|
171
|
+
responseParser: responseParsers.openaiStyle,
|
|
172
|
+
errorMessages: {
|
|
173
|
+
missingKey: "Missing OpenRouter API key.",
|
|
174
|
+
emptyResponse: "OpenRouter returned empty response",
|
|
175
|
+
badRequest: "OpenRouter rejected the request",
|
|
176
|
+
invalidKey: "Invalid OpenRouter API key",
|
|
177
|
+
rateLimit: "Too many requests to OpenRouter",
|
|
178
|
+
networkError: "Network error while contacting OpenRouter",
|
|
179
|
+
unknownError: "Unknown error occurred"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export class ProviderRegistry {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.providers = new Map();
|
|
4
|
+
this.providerFunctions = new Map();
|
|
5
|
+
}
|
|
6
|
+
register(name, provider) {
|
|
7
|
+
this.providers.set(name, provider);
|
|
8
|
+
this.providerFunctions.set(name, this.createProviderFunction(provider));
|
|
9
|
+
}
|
|
10
|
+
get(name) {
|
|
11
|
+
return this.providerFunctions.get(name);
|
|
12
|
+
}
|
|
13
|
+
getAvailableProviders() {
|
|
14
|
+
const available = [];
|
|
15
|
+
// Always prefer openrouter if present (matching original behavior)
|
|
16
|
+
if (this.providers.has("openrouter")) {
|
|
17
|
+
const openrouterProvider = this.providers.get("openrouter");
|
|
18
|
+
const apiKey = openrouterProvider['getApiKey']();
|
|
19
|
+
if (apiKey) {
|
|
20
|
+
available.push("openrouter");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Add others in order of their presence (matching original behavior)
|
|
24
|
+
const otherProviders = ['groq', 'openai', 'gemini', 'claude', 'deepseek', 'xai', 'perplexity', 'mistral'];
|
|
25
|
+
for (const providerName of otherProviders) {
|
|
26
|
+
if (this.providers.has(providerName)) {
|
|
27
|
+
const provider = this.providers.get(providerName);
|
|
28
|
+
const apiKey = provider['getApiKey']();
|
|
29
|
+
if (apiKey && !available.includes(providerName)) {
|
|
30
|
+
available.push(providerName);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return available;
|
|
35
|
+
}
|
|
36
|
+
createProviderFunction(provider) {
|
|
37
|
+
return async (prompt, model) => {
|
|
38
|
+
return provider.call(prompt, model);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Global registry instance
|
|
43
|
+
export const providerRegistry = new ProviderRegistry();
|