feat(llm): Added OpenAI provider

This commit is contained in:
2026-07-16 13:29:21 +05:30
parent 622fdfe2f1
commit 13155cba23
10 changed files with 844 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ const LLMConfigSchema = z.object({
GOOGLE_API_KEY: z.string().optional(),
OPENROUTER_API_KEY: z.string().optional(),
ANTHROPIC_API_KEY: z.string().optional(),
OPENAI_API_KEY: z.string().optional(),
});
export const llmConfig = LLMConfigSchema.parse(process.env);

View File

@@ -5,4 +5,5 @@ export * from "./providers/mock.js";
export * from "./providers/ollama.js";
export * from "./providers/openrouter.js";
export * from "./providers/anthropic.js";
export * from "./providers/openai.js";
export * from "./provider-manager.js";

View File

@@ -76,6 +76,13 @@ export const AVAILABLE_PROVIDERS: ModelProviderMeta[] = [
defaultModel: "gemini-2.5-flash",
defaultEmbeddingModel: "gemini-embedding-001",
},
{
id: "openai",
displayName: "OpenAI",
description: "Official OpenAI integration using @langchain/openai SDK",
defaultModel: "gpt-4o-mini",
defaultEmbeddingModel: "text-embedding-3-small",
},
{
id: "anthropic",
displayName: "Anthropic Claude",

View File

@@ -100,7 +100,9 @@ function getSettingsDb() {
const googleKey = process.env.GOOGLE_API_KEY;
const openRouterKey = process.env.OPENROUTER_API_KEY;
const anthropicKey = process.env.ANTHROPIC_API_KEY;
const openaiKey = process.env.OPENAI_API_KEY;
let hasInsertedGenerative = false;
let hasInsertedEmbedding = false;
if (googleKey && googleKey.trim()) {
const id = "provider-default-google";
@@ -137,6 +139,7 @@ function getSettingsDb() {
"embedding",
0,
);
hasInsertedEmbedding = true;
}
if (anthropicKey && anthropicKey.trim()) {
@@ -162,6 +165,50 @@ function getSettingsDb() {
}
}
if (openaiKey && openaiKey.trim()) {
const id = "provider-default-openai";
const isActive = hasInsertedGenerative ? 0 : 1;
db.prepare(
`
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`,
).run(
id,
"OpenAI (Env)",
"openai",
openaiKey.trim(),
isActive,
"gpt-4o-mini",
"generative",
128000,
);
if (isActive === 1) {
hasInsertedGenerative = true;
}
const embedId = "provider-default-openai-embed";
const isEmbedActive = hasInsertedEmbedding ? 0 : 1;
db.prepare(
`
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`,
).run(
embedId,
"OpenAI Embed (Env)",
"openai",
openaiKey.trim(),
isEmbedActive,
"text-embedding-3-small",
"embedding",
0,
);
if (isEmbedActive === 1) {
hasInsertedEmbedding = true;
}
}
if (openRouterKey && openRouterKey.trim()) {
const id = "provider-default-openrouter";
const isActive = hasInsertedGenerative ? 0 : 1;
@@ -416,7 +463,9 @@ export class ProviderManager {
const googleKey = process.env.GOOGLE_API_KEY;
const openRouterKey = process.env.OPENROUTER_API_KEY;
const anthropicKey = process.env.ANTHROPIC_API_KEY;
const openaiKey = process.env.OPENAI_API_KEY;
let hasInsertedGenerative = false;
let hasInsertedEmbedding = false;
if (googleKey && googleKey.trim()) {
const id = "provider-default-google";
@@ -453,6 +502,7 @@ export class ProviderManager {
"embedding",
0,
);
hasInsertedEmbedding = true;
}
if (anthropicKey && anthropicKey.trim()) {
@@ -478,6 +528,50 @@ export class ProviderManager {
}
}
if (openaiKey && openaiKey.trim()) {
const id = "provider-default-openai";
const isActive = hasInsertedGenerative ? 0 : 1;
db.prepare(
`
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`,
).run(
id,
"OpenAI (Env)",
"openai",
openaiKey.trim(),
isActive,
"gpt-4o-mini",
"generative",
128000,
);
if (isActive === 1) {
hasInsertedGenerative = true;
}
const embedId = "provider-default-openai-embed";
const isEmbedActive = hasInsertedEmbedding ? 0 : 1;
db.prepare(
`
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`,
).run(
embedId,
"OpenAI Embed (Env)",
"openai",
openaiKey.trim(),
isEmbedActive,
"text-embedding-3-small",
"embedding",
0,
);
if (isEmbedActive === 1) {
hasInsertedEmbedding = true;
}
}
if (openRouterKey && openRouterKey.trim()) {
const id = "provider-default-openrouter";
const isActive = hasInsertedGenerative ? 0 : 1;
@@ -608,6 +702,19 @@ export class ProviderManager {
maxContext: 0,
};
}
const openaiKey = process.env.OPENAI_API_KEY;
if (openaiKey && openaiKey.trim()) {
return {
id: "provider-default-env-embed-fallback",
name: "OpenAI Embed (Env Fallback)",
providerName: "openai",
apiKey: openaiKey.trim(),
isActive: true,
modelName: "text-embedding-3-small",
type: "embedding",
maxContext: 0,
};
}
return null;
}
@@ -624,6 +731,19 @@ export class ProviderManager {
maxContext: 32768,
};
}
const openaiKey = process.env.OPENAI_API_KEY;
if (openaiKey && openaiKey.trim()) {
return {
id: "provider-default-env-fallback",
name: "OpenAI (Env Fallback)",
providerName: "openai",
apiKey: openaiKey.trim(),
isActive: true,
modelName: "gpt-4o-mini",
type: "generative",
maxContext: 128000,
};
}
const anthropicKey = process.env.ANTHROPIC_API_KEY;
if (anthropicKey && anthropicKey.trim()) {
return {

View File

@@ -0,0 +1,160 @@
import { z } from "zod";
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import {
ILLMProvider,
LLMRequest,
LLMResponse,
LLMCallRecord,
IEmbeddingProvider,
} from "../llm.js";
import { llmConfig } from "../config.js";
import { ProviderManager } from "../provider-manager.js";
export class OpenAIProvider implements ILLMProvider {
static readonly providerId = "openai";
static readonly displayName = "OpenAI";
static readonly description =
"Official OpenAI integration using @langchain/openai SDK";
static readonly defaultModel = "gpt-4o-mini";
providerName = "OpenAI";
private model: ChatOpenAI;
private modelNameUsed: string;
private providerInstanceName?: string;
private maxContextUsed?: number;
lastCalls: LLMCallRecord[] = [];
constructor(
apiKey?: string,
modelName?: string,
providerInstanceName?: string,
maxContext?: number,
) {
let key = apiKey;
let model = modelName;
this.providerInstanceName = providerInstanceName;
this.maxContextUsed = maxContext;
if (!key) {
const active = ProviderManager.getActive("generative");
if (active && active.providerName === OpenAIProvider.providerId) {
key = active.apiKey;
if (!model) {
model = active.modelName;
}
if (!this.providerInstanceName) {
this.providerInstanceName = active.name;
}
if (this.maxContextUsed === undefined) {
this.maxContextUsed = active.maxContext;
}
}
}
if (!key) {
key = llmConfig.OPENAI_API_KEY;
if (!this.providerInstanceName && key) {
this.providerInstanceName = "Environment Variable";
}
}
if (!key) {
throw new Error(
"OPENAI_API_KEY is required to initialize OpenAIProvider",
);
}
this.modelNameUsed = model || OpenAIProvider.defaultModel;
this.model = new ChatOpenAI({
apiKey: key,
model: this.modelNameUsed,
});
}
async generateStructuredResponse<T extends z.ZodTypeAny>(
request: LLMRequest<T>,
): Promise<LLMResponse<z.infer<T>>> {
const structuredModel = this.model.withStructuredOutput(request.schema, {
includeRaw: true,
});
const result = (await structuredModel.invoke([
{ role: "system", content: request.systemPrompt },
{ role: "user", content: request.userContext },
])) as unknown as {
parsed?: z.infer<T>;
raw?: {
usage_metadata?: {
input_tokens?: number;
output_tokens?: number;
total_tokens?: number;
};
};
};
const parsed = result?.parsed;
const raw = result?.raw;
const usage = {
inputTokens: raw?.usage_metadata?.input_tokens || 0,
outputTokens: raw?.usage_metadata?.output_tokens || 0,
totalTokens: raw?.usage_metadata?.total_tokens || 0,
modelName: this.modelNameUsed,
providerInstanceName: this.providerInstanceName || "Default",
maxContext:
this.maxContextUsed !== undefined ? this.maxContextUsed : 128000,
};
this.lastCalls.push({
systemPrompt: request.systemPrompt,
userContext: request.userContext,
usage,
});
return { success: true, data: parsed, usage };
}
}
export class OpenAIEmbeddingProvider implements IEmbeddingProvider {
static readonly providerId = "openai";
static readonly displayName = "OpenAI Embeddings";
providerName = "OpenAI";
private model: OpenAIEmbeddings;
constructor(apiKey?: string, modelName?: string) {
let key = apiKey;
let model = modelName;
if (!key) {
const active = ProviderManager.getActive("embedding");
if (
active &&
active.providerName === OpenAIEmbeddingProvider.providerId
) {
key = active.apiKey;
if (!model) {
model = active.modelName;
}
}
}
if (!key) {
key = llmConfig.OPENAI_API_KEY;
}
if (!key) {
throw new Error(
"OPENAI_API_KEY is required to initialize OpenAIEmbeddingProvider",
);
}
this.model = new OpenAIEmbeddings({
apiKey: key,
model: model || "text-embedding-3-small",
});
}
async embed(text: string): Promise<number[]> {
return this.model.embedQuery(text);
}
}