@juspay/neurolink 7.4.0 → 7.6.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/CHANGELOG.md +15 -4
- package/README.md +182 -213
- package/dist/cli/factories/commandFactory.js +2 -0
- package/dist/cli/utils/interactiveSetup.js +43 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.js +2 -0
- package/dist/factories/providerRegistry.js +11 -0
- package/dist/lib/core/types.d.ts +2 -0
- package/dist/lib/core/types.js +2 -0
- package/dist/lib/factories/providerRegistry.js +11 -0
- package/dist/lib/neurolink.js +1 -0
- package/dist/lib/providers/index.d.ts +2 -0
- package/dist/lib/providers/index.js +2 -0
- package/dist/lib/providers/litellm.d.ts +43 -0
- package/dist/lib/providers/litellm.js +188 -0
- package/dist/lib/providers/openaiCompatible.d.ts +49 -0
- package/dist/lib/providers/openaiCompatible.js +260 -0
- package/dist/lib/utils/providerConfig.d.ts +4 -0
- package/dist/lib/utils/providerConfig.js +20 -0
- package/dist/lib/utils/providerUtils.js +4 -0
- package/dist/neurolink.js +1 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +2 -0
- package/dist/providers/litellm.d.ts +43 -0
- package/dist/providers/litellm.js +188 -0
- package/dist/providers/openaiCompatible.d.ts +49 -0
- package/dist/providers/openaiCompatible.js +261 -0
- package/dist/utils/providerConfig.d.ts +4 -0
- package/dist/utils/providerConfig.js +20 -0
- package/dist/utils/providerUtils.js +4 -0
- package/package.json +6 -2
package/dist/lib/core/types.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface TextGenerationResult {
|
|
|
42
42
|
export declare enum AIProviderName {
|
|
43
43
|
BEDROCK = "bedrock",
|
|
44
44
|
OPENAI = "openai",
|
|
45
|
+
OPENAI_COMPATIBLE = "openai-compatible",
|
|
45
46
|
VERTEX = "vertex",
|
|
46
47
|
ANTHROPIC = "anthropic",
|
|
47
48
|
AZURE = "azure",
|
|
@@ -49,6 +50,7 @@ export declare enum AIProviderName {
|
|
|
49
50
|
HUGGINGFACE = "huggingface",
|
|
50
51
|
OLLAMA = "ollama",
|
|
51
52
|
MISTRAL = "mistral",
|
|
53
|
+
LITELLM = "litellm",
|
|
52
54
|
AUTO = "auto"
|
|
53
55
|
}
|
|
54
56
|
/**
|
package/dist/lib/core/types.js
CHANGED
|
@@ -5,6 +5,7 @@ export var AIProviderName;
|
|
|
5
5
|
(function (AIProviderName) {
|
|
6
6
|
AIProviderName["BEDROCK"] = "bedrock";
|
|
7
7
|
AIProviderName["OPENAI"] = "openai";
|
|
8
|
+
AIProviderName["OPENAI_COMPATIBLE"] = "openai-compatible";
|
|
8
9
|
AIProviderName["VERTEX"] = "vertex";
|
|
9
10
|
AIProviderName["ANTHROPIC"] = "anthropic";
|
|
10
11
|
AIProviderName["AZURE"] = "azure";
|
|
@@ -12,6 +13,7 @@ export var AIProviderName;
|
|
|
12
13
|
AIProviderName["HUGGINGFACE"] = "huggingface";
|
|
13
14
|
AIProviderName["OLLAMA"] = "ollama";
|
|
14
15
|
AIProviderName["MISTRAL"] = "mistral";
|
|
16
|
+
AIProviderName["LITELLM"] = "litellm";
|
|
15
17
|
AIProviderName["AUTO"] = "auto";
|
|
16
18
|
})(AIProviderName || (AIProviderName = {}));
|
|
17
19
|
/**
|
|
@@ -71,6 +71,17 @@ export class ProviderRegistry {
|
|
|
71
71
|
const { OllamaProvider } = await import("../providers/ollama.js");
|
|
72
72
|
return new OllamaProvider(modelName);
|
|
73
73
|
}, process.env.OLLAMA_MODEL || "llama3.1:8b", ["ollama", "local"]);
|
|
74
|
+
// Register LiteLLM provider
|
|
75
|
+
ProviderFactory.registerProvider(AIProviderName.LITELLM, async (modelName, providerName, sdk) => {
|
|
76
|
+
const { LiteLLMProvider } = await import("../providers/litellm.js");
|
|
77
|
+
return new LiteLLMProvider(modelName, sdk);
|
|
78
|
+
}, process.env.LITELLM_MODEL || "openai/gpt-4o-mini", ["litellm"]);
|
|
79
|
+
// Register OpenAI Compatible provider
|
|
80
|
+
ProviderFactory.registerProvider(AIProviderName.OPENAI_COMPATIBLE, async (modelName, providerName, sdk) => {
|
|
81
|
+
const { OpenAICompatibleProvider } = await import("../providers/openaiCompatible.js");
|
|
82
|
+
return new OpenAICompatibleProvider(modelName, sdk);
|
|
83
|
+
}, process.env.OPENAI_COMPATIBLE_MODEL || undefined, // Enable auto-discovery when no model specified
|
|
84
|
+
["openai-compatible", "openrouter", "vllm", "compatible"]);
|
|
74
85
|
logger.debug("All providers registered successfully");
|
|
75
86
|
this.registered = true;
|
|
76
87
|
}
|
package/dist/lib/neurolink.js
CHANGED
|
@@ -701,6 +701,7 @@ export class NeuroLink {
|
|
|
701
701
|
"huggingface",
|
|
702
702
|
"ollama",
|
|
703
703
|
"mistral",
|
|
704
|
+
"litellm",
|
|
704
705
|
];
|
|
705
706
|
// 🚀 PERFORMANCE FIX: Test providers with controlled concurrency
|
|
706
707
|
// This reduces total time from 16s (sequential) to ~3s (parallel) while preventing resource exhaustion
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
export { GoogleVertexProvider as GoogleVertexAI } from "./googleVertex.js";
|
|
6
6
|
export { AmazonBedrockProvider as AmazonBedrock } from "./amazonBedrock.js";
|
|
7
7
|
export { OpenAIProvider as OpenAI } from "./openAI.js";
|
|
8
|
+
export { OpenAICompatibleProvider as OpenAICompatible } from "./openaiCompatible.js";
|
|
8
9
|
export { AnthropicProvider as AnthropicProvider } from "./anthropic.js";
|
|
9
10
|
export { AzureOpenAIProvider } from "./azureOpenai.js";
|
|
10
11
|
export { GoogleAIStudioProvider as GoogleAIStudio } from "./googleAiStudio.js";
|
|
@@ -19,6 +20,7 @@ export declare const PROVIDERS: {
|
|
|
19
20
|
readonly vertex: "GoogleVertexAI";
|
|
20
21
|
readonly bedrock: "AmazonBedrock";
|
|
21
22
|
readonly openai: "OpenAI";
|
|
23
|
+
readonly "openai-compatible": "OpenAICompatible";
|
|
22
24
|
readonly anthropic: "AnthropicProvider";
|
|
23
25
|
readonly azure: "AzureOpenAIProvider";
|
|
24
26
|
readonly "google-ai": "GoogleAIStudio";
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
export { GoogleVertexProvider as GoogleVertexAI } from "./googleVertex.js";
|
|
6
6
|
export { AmazonBedrockProvider as AmazonBedrock } from "./amazonBedrock.js";
|
|
7
7
|
export { OpenAIProvider as OpenAI } from "./openAI.js";
|
|
8
|
+
export { OpenAICompatibleProvider as OpenAICompatible } from "./openaiCompatible.js";
|
|
8
9
|
export { AnthropicProvider as AnthropicProvider } from "./anthropic.js";
|
|
9
10
|
export { AzureOpenAIProvider } from "./azureOpenai.js";
|
|
10
11
|
export { GoogleAIStudioProvider as GoogleAIStudio } from "./googleAiStudio.js";
|
|
@@ -18,6 +19,7 @@ export const PROVIDERS = {
|
|
|
18
19
|
vertex: "GoogleVertexAI",
|
|
19
20
|
bedrock: "AmazonBedrock",
|
|
20
21
|
openai: "OpenAI",
|
|
22
|
+
"openai-compatible": "OpenAICompatible",
|
|
21
23
|
anthropic: "AnthropicProvider",
|
|
22
24
|
azure: "AzureOpenAIProvider",
|
|
23
25
|
"google-ai": "GoogleAIStudio",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ZodType, ZodTypeDef } from "zod";
|
|
2
|
+
import { type Schema, type LanguageModelV1 } from "ai";
|
|
3
|
+
import type { AIProviderName } from "../core/types.js";
|
|
4
|
+
import type { StreamOptions, StreamResult } from "../types/streamTypes.js";
|
|
5
|
+
import { BaseProvider } from "../core/baseProvider.js";
|
|
6
|
+
/**
|
|
7
|
+
* LiteLLM Provider - BaseProvider Implementation
|
|
8
|
+
* Provides access to 100+ models via LiteLLM proxy server
|
|
9
|
+
*/
|
|
10
|
+
export declare class LiteLLMProvider extends BaseProvider {
|
|
11
|
+
private model;
|
|
12
|
+
constructor(modelName?: string, sdk?: unknown);
|
|
13
|
+
protected getProviderName(): AIProviderName;
|
|
14
|
+
protected getDefaultModel(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Returns the Vercel AI SDK model instance for LiteLLM
|
|
17
|
+
*/
|
|
18
|
+
protected getAISDKModel(): LanguageModelV1;
|
|
19
|
+
protected handleProviderError(error: unknown): Error;
|
|
20
|
+
/**
|
|
21
|
+
* LiteLLM supports tools for compatible models
|
|
22
|
+
*/
|
|
23
|
+
supportsTools(): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Provider-specific streaming implementation
|
|
26
|
+
* Note: This is only used when tools are disabled
|
|
27
|
+
*/
|
|
28
|
+
protected executeStream(options: StreamOptions, analysisSchema?: ZodType<unknown, ZodTypeDef, unknown> | Schema<unknown>): Promise<StreamResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Get available models from LiteLLM proxy server
|
|
31
|
+
*
|
|
32
|
+
* TODO: Implement dynamic fetching from LiteLLM's /v1/models endpoint.
|
|
33
|
+
* Currently returns a hardcoded list of commonly available models.
|
|
34
|
+
*
|
|
35
|
+
* Implementation would involve:
|
|
36
|
+
* 1. Fetch from `${baseURL}/v1/models`
|
|
37
|
+
* 2. Parse response to extract model IDs
|
|
38
|
+
* 3. Handle network errors gracefully
|
|
39
|
+
* 4. Cache results to avoid repeated API calls
|
|
40
|
+
*/
|
|
41
|
+
getAvailableModels(): Promise<string[]>;
|
|
42
|
+
private validateStreamOptions;
|
|
43
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
2
|
+
import { streamText } from "ai";
|
|
3
|
+
import { BaseProvider } from "../core/baseProvider.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
import { createTimeoutController, TimeoutError, } from "../utils/timeout.js";
|
|
6
|
+
import { DEFAULT_MAX_TOKENS } from "../core/constants.js";
|
|
7
|
+
import { getProviderModel } from "../utils/providerConfig.js";
|
|
8
|
+
import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
|
|
9
|
+
// Configuration helpers
|
|
10
|
+
const getLiteLLMConfig = () => {
|
|
11
|
+
return {
|
|
12
|
+
baseURL: process.env.LITELLM_BASE_URL || "http://localhost:4000",
|
|
13
|
+
apiKey: process.env.LITELLM_API_KEY || "sk-anything",
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Returns the default model name for LiteLLM.
|
|
18
|
+
*
|
|
19
|
+
* LiteLLM uses a 'provider/model' format for model names.
|
|
20
|
+
* For example:
|
|
21
|
+
* - 'openai/gpt-4o-mini'
|
|
22
|
+
* - 'openai/gpt-3.5-turbo'
|
|
23
|
+
* - 'anthropic/claude-3-sonnet-20240229'
|
|
24
|
+
* - 'google/gemini-pro'
|
|
25
|
+
*
|
|
26
|
+
* You can override the default by setting the LITELLM_MODEL environment variable.
|
|
27
|
+
*/
|
|
28
|
+
const getDefaultLiteLLMModel = () => {
|
|
29
|
+
return getProviderModel("LITELLM_MODEL", "openai/gpt-4o-mini");
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* LiteLLM Provider - BaseProvider Implementation
|
|
33
|
+
* Provides access to 100+ models via LiteLLM proxy server
|
|
34
|
+
*/
|
|
35
|
+
export class LiteLLMProvider extends BaseProvider {
|
|
36
|
+
model;
|
|
37
|
+
constructor(modelName, sdk) {
|
|
38
|
+
super(modelName, "litellm", sdk);
|
|
39
|
+
// Initialize LiteLLM using OpenAI SDK with explicit configuration
|
|
40
|
+
const config = getLiteLLMConfig();
|
|
41
|
+
// Create OpenAI SDK instance configured for LiteLLM proxy
|
|
42
|
+
// LiteLLM acts as a proxy server that implements the OpenAI-compatible API.
|
|
43
|
+
// To communicate with LiteLLM instead of the default OpenAI endpoint, we use createOpenAI
|
|
44
|
+
// with a custom baseURL and apiKey. This ensures all requests are routed through the LiteLLM
|
|
45
|
+
// proxy, allowing access to multiple models and custom authentication.
|
|
46
|
+
const customOpenAI = createOpenAI({
|
|
47
|
+
baseURL: config.baseURL,
|
|
48
|
+
apiKey: config.apiKey,
|
|
49
|
+
});
|
|
50
|
+
this.model = customOpenAI(this.modelName || getDefaultLiteLLMModel());
|
|
51
|
+
logger.debug("LiteLLM Provider initialized", {
|
|
52
|
+
modelName: this.modelName,
|
|
53
|
+
provider: this.providerName,
|
|
54
|
+
baseURL: config.baseURL,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
getProviderName() {
|
|
58
|
+
return "litellm";
|
|
59
|
+
}
|
|
60
|
+
getDefaultModel() {
|
|
61
|
+
return getDefaultLiteLLMModel();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Returns the Vercel AI SDK model instance for LiteLLM
|
|
65
|
+
*/
|
|
66
|
+
getAISDKModel() {
|
|
67
|
+
return this.model;
|
|
68
|
+
}
|
|
69
|
+
handleProviderError(error) {
|
|
70
|
+
if (error instanceof TimeoutError) {
|
|
71
|
+
return new Error(`LiteLLM request timed out: ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
// Check for timeout by error name and message as fallback
|
|
74
|
+
const errorRecord = error;
|
|
75
|
+
if (errorRecord?.name === "TimeoutError" ||
|
|
76
|
+
(typeof errorRecord?.message === "string" &&
|
|
77
|
+
errorRecord.message.includes("Timeout"))) {
|
|
78
|
+
return new Error(`LiteLLM request timed out: ${errorRecord?.message || "Unknown timeout"}`);
|
|
79
|
+
}
|
|
80
|
+
if (typeof errorRecord?.message === "string") {
|
|
81
|
+
if (errorRecord.message.includes("ECONNREFUSED") ||
|
|
82
|
+
errorRecord.message.includes("Failed to fetch")) {
|
|
83
|
+
return new Error("LiteLLM proxy server not available. Please start the LiteLLM proxy server at " +
|
|
84
|
+
`${process.env.LITELLM_BASE_URL || "http://localhost:4000"}`);
|
|
85
|
+
}
|
|
86
|
+
if (errorRecord.message.includes("API_KEY_INVALID") ||
|
|
87
|
+
errorRecord.message.includes("Invalid API key")) {
|
|
88
|
+
return new Error("Invalid LiteLLM configuration. Please check your LITELLM_API_KEY environment variable.");
|
|
89
|
+
}
|
|
90
|
+
if (errorRecord.message.includes("rate limit")) {
|
|
91
|
+
return new Error("LiteLLM rate limit exceeded. Please try again later.");
|
|
92
|
+
}
|
|
93
|
+
if (errorRecord.message.includes("model") &&
|
|
94
|
+
errorRecord.message.includes("not found")) {
|
|
95
|
+
return new Error(`Model '${this.modelName}' not available in LiteLLM proxy. ` +
|
|
96
|
+
"Please check your LiteLLM configuration and ensure the model is configured.");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return new Error(`LiteLLM error: ${errorRecord?.message || "Unknown error"}`);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* LiteLLM supports tools for compatible models
|
|
103
|
+
*/
|
|
104
|
+
supportsTools() {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Provider-specific streaming implementation
|
|
109
|
+
* Note: This is only used when tools are disabled
|
|
110
|
+
*/
|
|
111
|
+
async executeStream(options, analysisSchema) {
|
|
112
|
+
this.validateStreamOptions(options);
|
|
113
|
+
const startTime = Date.now();
|
|
114
|
+
const timeout = this.getTimeout(options);
|
|
115
|
+
const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
|
|
116
|
+
try {
|
|
117
|
+
const result = await streamText({
|
|
118
|
+
model: this.model,
|
|
119
|
+
prompt: options.input.text,
|
|
120
|
+
system: options.systemPrompt,
|
|
121
|
+
temperature: options.temperature,
|
|
122
|
+
maxTokens: options.maxTokens || DEFAULT_MAX_TOKENS,
|
|
123
|
+
tools: options.tools,
|
|
124
|
+
toolChoice: "auto",
|
|
125
|
+
abortSignal: timeoutController?.controller.signal,
|
|
126
|
+
});
|
|
127
|
+
timeoutController?.cleanup();
|
|
128
|
+
// Transform stream to match StreamResult interface
|
|
129
|
+
const transformedStream = async function* () {
|
|
130
|
+
for await (const chunk of result.textStream) {
|
|
131
|
+
yield { content: chunk };
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
// Create analytics promise that resolves after stream completion
|
|
135
|
+
const analyticsPromise = streamAnalyticsCollector.createAnalytics(this.providerName, this.modelName, result, Date.now() - startTime, {
|
|
136
|
+
requestId: `litellm-stream-${Date.now()}`,
|
|
137
|
+
streamingMode: true,
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
stream: transformedStream(),
|
|
141
|
+
provider: this.providerName,
|
|
142
|
+
model: this.modelName,
|
|
143
|
+
analytics: analyticsPromise,
|
|
144
|
+
metadata: {
|
|
145
|
+
startTime,
|
|
146
|
+
streamId: `litellm-${Date.now()}`,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
timeoutController?.cleanup();
|
|
152
|
+
throw this.handleProviderError(error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get available models from LiteLLM proxy server
|
|
157
|
+
*
|
|
158
|
+
* TODO: Implement dynamic fetching from LiteLLM's /v1/models endpoint.
|
|
159
|
+
* Currently returns a hardcoded list of commonly available models.
|
|
160
|
+
*
|
|
161
|
+
* Implementation would involve:
|
|
162
|
+
* 1. Fetch from `${baseURL}/v1/models`
|
|
163
|
+
* 2. Parse response to extract model IDs
|
|
164
|
+
* 3. Handle network errors gracefully
|
|
165
|
+
* 4. Cache results to avoid repeated API calls
|
|
166
|
+
*/
|
|
167
|
+
async getAvailableModels() {
|
|
168
|
+
// Hardcoded list of commonly available models
|
|
169
|
+
// TODO: Replace with dynamic fetch from LiteLLM proxy /v1/models endpoint
|
|
170
|
+
return [
|
|
171
|
+
"openai/gpt-4o",
|
|
172
|
+
"openai/gpt-4o-mini",
|
|
173
|
+
"anthropic/claude-3-5-sonnet",
|
|
174
|
+
"anthropic/claude-3-haiku",
|
|
175
|
+
"google/gemini-2.0-flash",
|
|
176
|
+
"mistral/mistral-large",
|
|
177
|
+
"mistral/mistral-medium",
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
// ===================
|
|
181
|
+
// PRIVATE VALIDATION METHODS
|
|
182
|
+
// ===================
|
|
183
|
+
validateStreamOptions(options) {
|
|
184
|
+
if (!options.input?.text || options.input.text.trim().length === 0) {
|
|
185
|
+
throw new Error("Input text is required and cannot be empty");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ZodType, ZodTypeDef } from "zod";
|
|
2
|
+
import { type Schema, type LanguageModelV1 } from "ai";
|
|
3
|
+
import type { AIProviderName } from "../core/types.js";
|
|
4
|
+
import type { StreamOptions, StreamResult } from "../types/streamTypes.js";
|
|
5
|
+
import { BaseProvider } from "../core/baseProvider.js";
|
|
6
|
+
/**
|
|
7
|
+
* OpenAI Compatible Provider - BaseProvider Implementation
|
|
8
|
+
* Provides access to any OpenAI-compatible endpoint (OpenRouter, vLLM, LiteLLM, etc.)
|
|
9
|
+
*/
|
|
10
|
+
export declare class OpenAICompatibleProvider extends BaseProvider {
|
|
11
|
+
private model?;
|
|
12
|
+
private config;
|
|
13
|
+
private discoveredModel?;
|
|
14
|
+
private customOpenAI;
|
|
15
|
+
constructor(modelName?: string, sdk?: unknown);
|
|
16
|
+
protected getProviderName(): AIProviderName;
|
|
17
|
+
protected getDefaultModel(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Returns the Vercel AI SDK model instance for OpenAI Compatible endpoints
|
|
20
|
+
* Handles auto-discovery if no model was specified
|
|
21
|
+
*/
|
|
22
|
+
protected getAISDKModel(): Promise<LanguageModelV1>;
|
|
23
|
+
protected handleProviderError(error: unknown): Error;
|
|
24
|
+
/**
|
|
25
|
+
* OpenAI Compatible endpoints support tools for compatible models
|
|
26
|
+
*/
|
|
27
|
+
supportsTools(): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Provider-specific streaming implementation
|
|
30
|
+
* Note: This is only used when tools are disabled
|
|
31
|
+
*/
|
|
32
|
+
protected executeStream(options: StreamOptions, analysisSchema?: ZodType<unknown, ZodTypeDef, unknown> | Schema<unknown>): Promise<StreamResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Get available models from OpenAI Compatible endpoint
|
|
35
|
+
*
|
|
36
|
+
* Fetches from the /v1/models endpoint to discover available models.
|
|
37
|
+
* This is useful for auto-discovery when no model is specified.
|
|
38
|
+
*/
|
|
39
|
+
getAvailableModels(): Promise<string[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Get the first available model for auto-selection
|
|
42
|
+
*/
|
|
43
|
+
getFirstAvailableModel(): Promise<string>;
|
|
44
|
+
/**
|
|
45
|
+
* Fallback models when discovery fails
|
|
46
|
+
*/
|
|
47
|
+
private getFallbackModels;
|
|
48
|
+
private validateStreamOptions;
|
|
49
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
2
|
+
import { streamText } from "ai";
|
|
3
|
+
import { BaseProvider } from "../core/baseProvider.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
import { createTimeoutController, TimeoutError, } from "../utils/timeout.js";
|
|
6
|
+
import { DEFAULT_MAX_TOKENS } from "../core/constants.js";
|
|
7
|
+
import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
|
|
8
|
+
// Constants
|
|
9
|
+
const FALLBACK_OPENAI_COMPATIBLE_MODEL = "gpt-3.5-turbo";
|
|
10
|
+
// Configuration helpers
|
|
11
|
+
const getOpenAICompatibleConfig = () => {
|
|
12
|
+
const baseURL = process.env.OPENAI_COMPATIBLE_BASE_URL;
|
|
13
|
+
const apiKey = process.env.OPENAI_COMPATIBLE_API_KEY;
|
|
14
|
+
if (!baseURL) {
|
|
15
|
+
throw new Error("OPENAI_COMPATIBLE_BASE_URL environment variable is required. " +
|
|
16
|
+
"Please set it to your OpenAI-compatible endpoint (e.g., https://api.openrouter.ai/api/v1)");
|
|
17
|
+
}
|
|
18
|
+
if (!apiKey) {
|
|
19
|
+
throw new Error("OPENAI_COMPATIBLE_API_KEY environment variable is required. " +
|
|
20
|
+
"Please set it to your API key for the OpenAI-compatible service.");
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
baseURL,
|
|
24
|
+
apiKey,
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Returns the default model name for OpenAI Compatible endpoints.
|
|
29
|
+
*
|
|
30
|
+
* Returns undefined if no model is specified via OPENAI_COMPATIBLE_MODEL environment variable,
|
|
31
|
+
* which triggers auto-discovery from the /v1/models endpoint.
|
|
32
|
+
*/
|
|
33
|
+
const getDefaultOpenAICompatibleModel = () => {
|
|
34
|
+
return process.env.OPENAI_COMPATIBLE_MODEL || undefined;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* OpenAI Compatible Provider - BaseProvider Implementation
|
|
38
|
+
* Provides access to any OpenAI-compatible endpoint (OpenRouter, vLLM, LiteLLM, etc.)
|
|
39
|
+
*/
|
|
40
|
+
export class OpenAICompatibleProvider extends BaseProvider {
|
|
41
|
+
model;
|
|
42
|
+
config;
|
|
43
|
+
discoveredModel;
|
|
44
|
+
customOpenAI;
|
|
45
|
+
constructor(modelName, sdk) {
|
|
46
|
+
super(modelName, "openai-compatible", sdk);
|
|
47
|
+
// Initialize OpenAI Compatible configuration
|
|
48
|
+
this.config = getOpenAICompatibleConfig();
|
|
49
|
+
// Create OpenAI SDK instance configured for custom endpoint
|
|
50
|
+
// This allows us to use any OpenAI-compatible API by simply changing the baseURL
|
|
51
|
+
this.customOpenAI = createOpenAI({
|
|
52
|
+
baseURL: this.config.baseURL,
|
|
53
|
+
apiKey: this.config.apiKey,
|
|
54
|
+
});
|
|
55
|
+
logger.debug("OpenAI Compatible Provider initialized", {
|
|
56
|
+
modelName: this.modelName,
|
|
57
|
+
provider: this.providerName,
|
|
58
|
+
baseURL: this.config.baseURL,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
getProviderName() {
|
|
62
|
+
return "openai-compatible";
|
|
63
|
+
}
|
|
64
|
+
getDefaultModel() {
|
|
65
|
+
// Return empty string when no model is explicitly configured to enable auto-discovery
|
|
66
|
+
return getDefaultOpenAICompatibleModel() || "";
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Returns the Vercel AI SDK model instance for OpenAI Compatible endpoints
|
|
70
|
+
* Handles auto-discovery if no model was specified
|
|
71
|
+
*/
|
|
72
|
+
async getAISDKModel() {
|
|
73
|
+
// If model instance doesn't exist yet, create it
|
|
74
|
+
if (!this.model) {
|
|
75
|
+
let modelToUse;
|
|
76
|
+
// Check if a model was explicitly specified via constructor or env var
|
|
77
|
+
const explicitModel = this.modelName || getDefaultOpenAICompatibleModel();
|
|
78
|
+
// Treat empty string as no model specified (trigger auto-discovery)
|
|
79
|
+
if (explicitModel && explicitModel.trim() !== "") {
|
|
80
|
+
// Use the explicitly specified model
|
|
81
|
+
modelToUse = explicitModel;
|
|
82
|
+
logger.debug(`Using specified model: ${modelToUse}`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// No model specified, auto-discover from endpoint
|
|
86
|
+
try {
|
|
87
|
+
const availableModels = await this.getAvailableModels();
|
|
88
|
+
if (availableModels.length > 0) {
|
|
89
|
+
this.discoveredModel = availableModels[0];
|
|
90
|
+
modelToUse = this.discoveredModel;
|
|
91
|
+
logger.info(`🔍 Auto-discovered model: ${modelToUse} from ${availableModels.length} available models`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// Fall back to a common default if no models discovered
|
|
95
|
+
modelToUse = FALLBACK_OPENAI_COMPATIBLE_MODEL;
|
|
96
|
+
logger.warn(`No models discovered, using fallback: ${modelToUse}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
logger.warn("Model auto-discovery failed, using fallback:", error);
|
|
101
|
+
modelToUse = FALLBACK_OPENAI_COMPATIBLE_MODEL;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Create the model instance
|
|
105
|
+
this.model = this.customOpenAI(modelToUse);
|
|
106
|
+
}
|
|
107
|
+
return this.model;
|
|
108
|
+
}
|
|
109
|
+
handleProviderError(error) {
|
|
110
|
+
if (error instanceof TimeoutError) {
|
|
111
|
+
return new Error(`OpenAI Compatible request timed out: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
// Check for timeout by error name and message as fallback
|
|
114
|
+
const errorRecord = error;
|
|
115
|
+
if (errorRecord?.name === "TimeoutError" ||
|
|
116
|
+
(typeof errorRecord?.message === "string" &&
|
|
117
|
+
errorRecord.message.includes("Timeout"))) {
|
|
118
|
+
return new Error(`OpenAI Compatible request timed out: ${errorRecord?.message || "Unknown timeout"}`);
|
|
119
|
+
}
|
|
120
|
+
if (typeof errorRecord?.message === "string") {
|
|
121
|
+
if (errorRecord.message.includes("ECONNREFUSED") ||
|
|
122
|
+
errorRecord.message.includes("Failed to fetch")) {
|
|
123
|
+
return new Error(`OpenAI Compatible endpoint not available. Please check your OPENAI_COMPATIBLE_BASE_URL: ${this.config.baseURL}`);
|
|
124
|
+
}
|
|
125
|
+
if (errorRecord.message.includes("API_KEY_INVALID") ||
|
|
126
|
+
errorRecord.message.includes("Invalid API key") ||
|
|
127
|
+
errorRecord.message.includes("Unauthorized")) {
|
|
128
|
+
return new Error("Invalid OpenAI Compatible API key. Please check your OPENAI_COMPATIBLE_API_KEY environment variable.");
|
|
129
|
+
}
|
|
130
|
+
if (errorRecord.message.includes("rate limit")) {
|
|
131
|
+
return new Error("OpenAI Compatible rate limit exceeded. Please try again later.");
|
|
132
|
+
}
|
|
133
|
+
if (errorRecord.message.includes("model") &&
|
|
134
|
+
(errorRecord.message.includes("not found") ||
|
|
135
|
+
errorRecord.message.includes("does not exist"))) {
|
|
136
|
+
return new Error(`Model '${this.modelName}' not available on OpenAI Compatible endpoint. ` +
|
|
137
|
+
"Please check available models or use getAvailableModels() to see supported models.");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return new Error(`OpenAI Compatible error: ${errorRecord?.message || "Unknown error"}`);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* OpenAI Compatible endpoints support tools for compatible models
|
|
144
|
+
*/
|
|
145
|
+
supportsTools() {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Provider-specific streaming implementation
|
|
150
|
+
* Note: This is only used when tools are disabled
|
|
151
|
+
*/
|
|
152
|
+
async executeStream(options, analysisSchema) {
|
|
153
|
+
this.validateStreamOptions(options);
|
|
154
|
+
const startTime = Date.now();
|
|
155
|
+
const timeout = this.getTimeout(options);
|
|
156
|
+
const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
|
|
157
|
+
try {
|
|
158
|
+
const model = await this.getAISDKModel();
|
|
159
|
+
const result = await streamText({
|
|
160
|
+
model,
|
|
161
|
+
prompt: options.input.text,
|
|
162
|
+
system: options.systemPrompt,
|
|
163
|
+
temperature: options.temperature,
|
|
164
|
+
maxTokens: options.maxTokens || DEFAULT_MAX_TOKENS,
|
|
165
|
+
tools: options.tools,
|
|
166
|
+
toolChoice: "auto",
|
|
167
|
+
abortSignal: timeoutController?.controller.signal,
|
|
168
|
+
});
|
|
169
|
+
timeoutController?.cleanup();
|
|
170
|
+
// Transform stream to match StreamResult interface
|
|
171
|
+
const transformedStream = async function* () {
|
|
172
|
+
for await (const chunk of result.textStream) {
|
|
173
|
+
yield { content: chunk };
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
// Create analytics promise that resolves after stream completion
|
|
177
|
+
const analyticsPromise = streamAnalyticsCollector.createAnalytics(this.providerName, this.modelName, result, Date.now() - startTime, {
|
|
178
|
+
requestId: `openai-compatible-stream-${Date.now()}`,
|
|
179
|
+
streamingMode: true,
|
|
180
|
+
});
|
|
181
|
+
return {
|
|
182
|
+
stream: transformedStream(),
|
|
183
|
+
provider: this.providerName,
|
|
184
|
+
model: this.modelName,
|
|
185
|
+
analytics: analyticsPromise,
|
|
186
|
+
metadata: {
|
|
187
|
+
startTime,
|
|
188
|
+
streamId: `openai-compatible-${Date.now()}`,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
timeoutController?.cleanup();
|
|
194
|
+
throw this.handleProviderError(error);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Get available models from OpenAI Compatible endpoint
|
|
199
|
+
*
|
|
200
|
+
* Fetches from the /v1/models endpoint to discover available models.
|
|
201
|
+
* This is useful for auto-discovery when no model is specified.
|
|
202
|
+
*/
|
|
203
|
+
async getAvailableModels() {
|
|
204
|
+
try {
|
|
205
|
+
const modelsUrl = new URL("/v1/models", this.config.baseURL).toString();
|
|
206
|
+
logger.debug(`Fetching available models from: ${modelsUrl}`);
|
|
207
|
+
const response = await fetch(modelsUrl, {
|
|
208
|
+
headers: {
|
|
209
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
210
|
+
"Content-Type": "application/json",
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
logger.warn(`Models endpoint returned ${response.status}: ${response.statusText}`);
|
|
215
|
+
return this.getFallbackModels();
|
|
216
|
+
}
|
|
217
|
+
const data = await response.json();
|
|
218
|
+
if (!data.data || !Array.isArray(data.data)) {
|
|
219
|
+
logger.warn("Invalid models response format");
|
|
220
|
+
return this.getFallbackModels();
|
|
221
|
+
}
|
|
222
|
+
const models = data.data.map((model) => model.id).filter(Boolean);
|
|
223
|
+
logger.debug(`Discovered ${models.length} models:`, models);
|
|
224
|
+
return models.length > 0 ? models : this.getFallbackModels();
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
logger.warn(`Failed to fetch models from OpenAI Compatible endpoint:`, error);
|
|
228
|
+
return this.getFallbackModels();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get the first available model for auto-selection
|
|
233
|
+
*/
|
|
234
|
+
async getFirstAvailableModel() {
|
|
235
|
+
const models = await this.getAvailableModels();
|
|
236
|
+
return models[0] || FALLBACK_OPENAI_COMPATIBLE_MODEL;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Fallback models when discovery fails
|
|
240
|
+
*/
|
|
241
|
+
getFallbackModels() {
|
|
242
|
+
return [
|
|
243
|
+
"gpt-4o",
|
|
244
|
+
"gpt-4o-mini",
|
|
245
|
+
"gpt-4-turbo",
|
|
246
|
+
FALLBACK_OPENAI_COMPATIBLE_MODEL,
|
|
247
|
+
"claude-3-5-sonnet",
|
|
248
|
+
"claude-3-haiku",
|
|
249
|
+
"gemini-pro",
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
// ===================
|
|
253
|
+
// PRIVATE VALIDATION METHODS
|
|
254
|
+
// ===================
|
|
255
|
+
validateStreamOptions(options) {
|
|
256
|
+
if (!options.input?.text || options.input.text.trim().length === 0) {
|
|
257
|
+
throw new Error("Input text is required and cannot be empty");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -91,6 +91,10 @@ export declare function createAzureAPIKeyConfig(): ProviderConfigOptions;
|
|
|
91
91
|
* Creates Azure OpenAI Endpoint configuration
|
|
92
92
|
*/
|
|
93
93
|
export declare function createAzureEndpointConfig(): ProviderConfigOptions;
|
|
94
|
+
/**
|
|
95
|
+
* Creates OpenAI Compatible provider configuration
|
|
96
|
+
*/
|
|
97
|
+
export declare function createOpenAICompatibleConfig(): ProviderConfigOptions;
|
|
94
98
|
/**
|
|
95
99
|
* Creates Google Vertex Project ID configuration
|
|
96
100
|
*/
|
|
@@ -273,6 +273,26 @@ export function createAzureEndpointConfig() {
|
|
|
273
273
|
],
|
|
274
274
|
};
|
|
275
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Creates OpenAI Compatible provider configuration
|
|
278
|
+
*/
|
|
279
|
+
export function createOpenAICompatibleConfig() {
|
|
280
|
+
return {
|
|
281
|
+
providerName: "OpenAI Compatible",
|
|
282
|
+
envVarName: "OPENAI_COMPATIBLE_API_KEY",
|
|
283
|
+
setupUrl: "https://openrouter.ai/",
|
|
284
|
+
description: "OpenAI-compatible API credentials",
|
|
285
|
+
instructions: [
|
|
286
|
+
"1. Set OPENAI_COMPATIBLE_BASE_URL to your endpoint (e.g., https://api.openrouter.ai/api/v1)",
|
|
287
|
+
"2. Get API key from your OpenAI-compatible service:",
|
|
288
|
+
" • OpenRouter: https://openrouter.ai/keys",
|
|
289
|
+
" • vLLM: Use any value for local deployments",
|
|
290
|
+
" • LiteLLM: Check your LiteLLM server configuration",
|
|
291
|
+
"3. Set OPENAI_COMPATIBLE_API_KEY to your API key",
|
|
292
|
+
"4. Optionally set OPENAI_COMPATIBLE_MODEL (will auto-discover if not set)",
|
|
293
|
+
],
|
|
294
|
+
};
|
|
295
|
+
}
|
|
276
296
|
/**
|
|
277
297
|
* Creates Google Vertex Project ID configuration
|
|
278
298
|
*/
|