262 lines
6.8 KiB
TypeScript
262 lines
6.8 KiB
TypeScript
export type ProviderKind = "openai-compatible" | "anthropic" | "gemini";
|
|
|
|
export type ProviderId =
|
|
| "deepseek"
|
|
| "zhipu"
|
|
| "openai"
|
|
| "anthropic"
|
|
| "gemini"
|
|
| "custom-openai";
|
|
|
|
export type ModelProvider = {
|
|
id: ProviderId;
|
|
name: string;
|
|
kind: ProviderKind;
|
|
baseUrl?: string;
|
|
apiKeyEnv: string;
|
|
apiKey?: string;
|
|
configured: boolean;
|
|
};
|
|
|
|
export type ChatModel = {
|
|
id: string;
|
|
label: string;
|
|
providerId: ProviderId;
|
|
providerName: string;
|
|
providerKind: ProviderKind;
|
|
model: string;
|
|
baseUrl: string;
|
|
enabled: boolean;
|
|
configured: boolean;
|
|
apiKeyConfigured: boolean;
|
|
apiKeyPreview: string;
|
|
apiKeySource: "model" | "provider-env" | "none";
|
|
configUpdatedAt?: string;
|
|
accent: "red" | "green" | "blue" | "gold" | "ink";
|
|
};
|
|
|
|
import { getSavedModelConfig, maskApiKey } from "./modelConfigStore.js";
|
|
|
|
const env = (key: string, fallback = "") => process.env[key]?.trim() || fallback;
|
|
|
|
export const providers = (): ModelProvider[] => [
|
|
{
|
|
id: "deepseek",
|
|
name: "DeepSeek",
|
|
kind: "openai-compatible",
|
|
baseUrl: env("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
|
|
apiKeyEnv: "DEEPSEEK_API_KEY",
|
|
configured: Boolean(env("DEEPSEEK_API_KEY"))
|
|
},
|
|
{
|
|
id: "zhipu",
|
|
name: "Zhipu GLM",
|
|
kind: "openai-compatible",
|
|
baseUrl: env("ZHIPU_BASE_URL", "https://open.bigmodel.cn/api/paas/v4"),
|
|
apiKeyEnv: "ZHIPU_API_KEY",
|
|
configured: Boolean(env("ZHIPU_API_KEY"))
|
|
},
|
|
{
|
|
id: "openai",
|
|
name: "OpenAI",
|
|
kind: "openai-compatible",
|
|
baseUrl: env("OPENAI_BASE_URL", "https://api.openai.com/v1"),
|
|
apiKeyEnv: "OPENAI_API_KEY",
|
|
configured: Boolean(env("OPENAI_API_KEY"))
|
|
},
|
|
{
|
|
id: "anthropic",
|
|
name: "Anthropic",
|
|
kind: "anthropic",
|
|
baseUrl: "https://api.anthropic.com/v1",
|
|
apiKeyEnv: "ANTHROPIC_API_KEY",
|
|
configured: Boolean(env("ANTHROPIC_API_KEY"))
|
|
},
|
|
{
|
|
id: "gemini",
|
|
name: "Google Gemini",
|
|
kind: "gemini",
|
|
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
apiKeyEnv: "GEMINI_API_KEY",
|
|
configured: Boolean(env("GEMINI_API_KEY"))
|
|
}
|
|
];
|
|
|
|
const resolveModel = (
|
|
model: Omit<
|
|
ChatModel,
|
|
| "providerName"
|
|
| "providerKind"
|
|
| "configured"
|
|
| "baseUrl"
|
|
| "enabled"
|
|
| "apiKeyConfigured"
|
|
| "apiKeyPreview"
|
|
| "apiKeySource"
|
|
>,
|
|
provider: ModelProvider
|
|
): ChatModel => {
|
|
const saved = getSavedModelConfig(model.id);
|
|
const envApiKey = env(provider.apiKeyEnv);
|
|
const apiKey = saved?.apiKey || envApiKey;
|
|
const apiKeySource = saved?.apiKey ? "model" : envApiKey ? "provider-env" : "none";
|
|
|
|
return {
|
|
...model,
|
|
providerName: provider.name,
|
|
providerKind: provider.kind,
|
|
model: saved?.model || model.model,
|
|
baseUrl: saved?.baseUrl || provider.baseUrl || "",
|
|
enabled: saved?.enabled ?? true,
|
|
configured: Boolean(apiKey),
|
|
apiKeyConfigured: Boolean(apiKey),
|
|
apiKeyPreview: maskApiKey(apiKey),
|
|
apiKeySource,
|
|
configUpdatedAt: saved?.updatedAt
|
|
};
|
|
};
|
|
|
|
export const registry = (): ChatModel[] => {
|
|
const providerMap = new Map(providers().map((provider) => [provider.id, provider]));
|
|
const baseModels: Array<
|
|
Omit<
|
|
ChatModel,
|
|
| "providerName"
|
|
| "providerKind"
|
|
| "configured"
|
|
| "baseUrl"
|
|
| "enabled"
|
|
| "apiKeyConfigured"
|
|
| "apiKeyPreview"
|
|
| "apiKeySource"
|
|
>
|
|
> = [
|
|
{
|
|
id: "deepseek-v4-pro",
|
|
label: "DeepSeekV4-pro",
|
|
providerId: "deepseek",
|
|
model: env("DEEPSEEK_MODEL_V4_PRO", "DeepSeekV4-pro"),
|
|
accent: "red"
|
|
},
|
|
{
|
|
id: "deepseek-v4-flash",
|
|
label: "DeepSeekV4-flash",
|
|
providerId: "deepseek",
|
|
model: env("DEEPSEEK_MODEL_V4_FLASH", "DeepSeekV4-flash"),
|
|
accent: "red"
|
|
},
|
|
{
|
|
id: "glm-5-1",
|
|
label: "GLM5.1",
|
|
providerId: "zhipu",
|
|
model: env("ZHIPU_MODEL_GLM_5_1", "GLM5.1"),
|
|
accent: "green"
|
|
},
|
|
{
|
|
id: "gpt-5-5",
|
|
label: "GPT-5.5",
|
|
providerId: "openai",
|
|
model: env("OPENAI_MODEL_GPT_5_5", "gpt-5.5"),
|
|
accent: "blue"
|
|
},
|
|
{
|
|
id: "claude-opus-4-7",
|
|
label: "Claude Opus 4.7",
|
|
providerId: "anthropic",
|
|
model: env("ANTHROPIC_MODEL_CLAUDE_OPUS_4_7", "claude-opus-4.7"),
|
|
accent: "gold"
|
|
},
|
|
{
|
|
id: "gemini-3-1-pro",
|
|
label: "Gemini-3.1-pro",
|
|
providerId: "gemini",
|
|
model: env("GEMINI_MODEL_3_1_PRO", "Gemini-3.1-pro"),
|
|
accent: "ink"
|
|
},
|
|
{
|
|
id: "gemini-3-5-flash",
|
|
label: "gemini3.5-flash",
|
|
providerId: "gemini",
|
|
model: env("GEMINI_MODEL_3_5_FLASH", "gemini3.5-flash"),
|
|
accent: "ink"
|
|
}
|
|
];
|
|
|
|
const models = baseModels.map((model) => {
|
|
const provider = providerMap.get(model.providerId);
|
|
if (!provider) {
|
|
throw new Error(`Missing provider for ${model.id}`);
|
|
}
|
|
|
|
return resolveModel(model, provider);
|
|
});
|
|
|
|
const customIds = env("CUSTOM_OPENAI_MODEL_IDS")
|
|
.split(",")
|
|
.map((value) => value.trim())
|
|
.filter(Boolean);
|
|
|
|
if (customIds.length && env("CUSTOM_OPENAI_BASE_URL")) {
|
|
const customConfigured = Boolean(env("CUSTOM_OPENAI_API_KEY"));
|
|
models.push(
|
|
...customIds.map((modelId) =>
|
|
resolveModel(
|
|
{
|
|
id: `custom-${modelId.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`,
|
|
label: modelId,
|
|
providerId: "custom-openai" as const,
|
|
model: modelId,
|
|
configUpdatedAt: undefined,
|
|
accent: "blue" as const
|
|
},
|
|
{
|
|
id: "custom-openai",
|
|
name: env("CUSTOM_OPENAI_NAME", "Custom OpenAI-compatible"),
|
|
kind: "openai-compatible",
|
|
baseUrl: env("CUSTOM_OPENAI_BASE_URL"),
|
|
apiKeyEnv: "CUSTOM_OPENAI_API_KEY",
|
|
configured: customConfigured
|
|
}
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
return models;
|
|
};
|
|
|
|
export const findModel = (modelId: string) => registry().find((model) => model.id === modelId);
|
|
|
|
export const providerForModel = (model: ChatModel): ModelProvider => {
|
|
const saved = getSavedModelConfig(model.id);
|
|
|
|
if (model.providerId === "custom-openai") {
|
|
const envApiKey = env("CUSTOM_OPENAI_API_KEY");
|
|
return {
|
|
id: "custom-openai",
|
|
name: env("CUSTOM_OPENAI_NAME", "Custom OpenAI-compatible"),
|
|
kind: "openai-compatible",
|
|
baseUrl: saved?.baseUrl || env("CUSTOM_OPENAI_BASE_URL"),
|
|
apiKeyEnv: "CUSTOM_OPENAI_API_KEY",
|
|
apiKey: saved?.apiKey,
|
|
configured: Boolean(saved?.apiKey || envApiKey)
|
|
};
|
|
}
|
|
|
|
const provider = providers().find((item) => item.id === model.providerId);
|
|
if (!provider) {
|
|
throw new Error(`Provider not found for ${model.label}`);
|
|
}
|
|
|
|
const envApiKey = env(provider.apiKeyEnv);
|
|
|
|
return {
|
|
...provider,
|
|
baseUrl: saved?.baseUrl || provider.baseUrl,
|
|
apiKey: saved?.apiKey,
|
|
configured: Boolean(saved?.apiKey || envApiKey)
|
|
};
|
|
};
|
|
|
|
export const apiKeyForProvider = (provider: ModelProvider) => provider.apiKey || env(provider.apiKeyEnv);
|