Files
Jyotisha/frontend/src/mastra/model.ts
T
2026-07-17 13:07:17 +08:00

249 lines
7.2 KiB
TypeScript

import type { MastraModelConfig } from "@mastra/core/llm";
import { z } from "zod";
type Environment = Readonly<Record<string, string | undefined>>;
type LanguageModelMode = "openai" | "compatible";
export type PublicLanguageModel = {
readonly id: string;
readonly label: string;
readonly description: string;
readonly creditCost: 1;
readonly isDefault: boolean;
};
export type ResolvedLanguageModel = PublicLanguageModel & {
readonly mode: LanguageModelMode;
readonly model: MastraModelConfig;
};
export type LanguageModelCatalog = {
readonly models: readonly ResolvedLanguageModel[];
readonly publicModels: readonly PublicLanguageModel[];
readonly defaultModelId: string | null;
readonly issues: readonly string[];
};
const modelIdSchema = z.string().trim().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/);
const apiKeyEnvironmentNameSchema = z.string().regex(/^[A-Z][A-Z0-9_]*$/);
const sharedCatalogFields = {
id: modelIdSchema,
label: z.string().trim().min(1).max(60),
description: z.string().trim().max(100).default(""),
apiKeyEnv: apiKeyEnvironmentNameSchema,
model: z.string().trim().min(1).max(120),
creditCost: z.literal(1),
};
const catalogEntrySchema = z.discriminatedUnion("provider", [
z.object({
...sharedCatalogFields,
provider: z.literal("openai"),
}).strict(),
z.object({
...sharedCatalogFields,
provider: z.literal("openai-compatible"),
baseURL: z.string().url().refine((value) => value.startsWith("https://")),
}).strict(),
]);
type CatalogEntry = z.infer<typeof catalogEntrySchema>;
function environmentValue(environment: Environment, name: string) {
return environment[name]?.trim() ?? "";
}
function publicModel(model: ResolvedLanguageModel): PublicLanguageModel {
return {
id: model.id,
label: model.label,
description: model.description,
creditCost: model.creditCost,
isDefault: model.isDefault,
};
}
function resolveCatalogEntry(
entry: CatalogEntry,
apiKey: string,
isDefault: boolean,
): ResolvedLanguageModel {
const shared = {
id: entry.id,
label: entry.label,
description: entry.description,
creditCost: entry.creditCost,
isDefault,
} as const;
switch (entry.provider) {
case "openai":
return {
...shared,
mode: "openai",
model: entry.model,
};
case "openai-compatible":
return {
...shared,
mode: "compatible",
model: {
providerId: entry.id,
modelId: entry.model,
url: entry.baseURL,
apiKey,
},
};
}
}
function resolveExplicitCatalog(environment: Environment, rawCatalog: string): LanguageModelCatalog {
let parsed: unknown;
try {
parsed = JSON.parse(rawCatalog);
} catch {
return { models: [], publicModels: [], defaultModelId: null, issues: ["catalog_json_invalid"] };
}
if (!Array.isArray(parsed)) {
return { models: [], publicModels: [], defaultModelId: null, issues: ["catalog_not_array"] };
}
const defaultModelId = environmentValue(environment, "LLM_DEFAULT_MODEL_ID");
const issues: string[] = [];
const seenIds = new Set<string>();
const models: ResolvedLanguageModel[] = [];
parsed.forEach((value, index) => {
const parsedEntry = catalogEntrySchema.safeParse(value);
if (!parsedEntry.success) {
issues.push(`catalog_entry_invalid:${index}`);
return;
}
if (seenIds.has(parsedEntry.data.id)) {
issues.push(`catalog_entry_duplicate:${index}`);
return;
}
seenIds.add(parsedEntry.data.id);
const apiKey = environmentValue(environment, parsedEntry.data.apiKeyEnv);
if (!apiKey) {
issues.push(`catalog_entry_secret_missing:${index}`);
return;
}
models.push(resolveCatalogEntry(parsedEntry.data, apiKey, parsedEntry.data.id === defaultModelId));
});
const resolvedDefault = models.some((model) => model.id === defaultModelId)
? defaultModelId
: null;
if (!resolvedDefault) issues.push("default_model_unavailable");
return {
models,
publicModels: models.map(publicModel),
defaultModelId: resolvedDefault,
issues,
};
}
function resolveLegacyCatalog(environment: Environment): LanguageModelCatalog {
const baseURL = environmentValue(environment, "LLM_BASE_URL");
const apiKey = environmentValue(environment, "LLM_API_KEY");
const modelId = environmentValue(environment, "LLM_MODEL");
const hasCompatibleSetting = Boolean(baseURL || apiKey || modelId);
if (hasCompatibleSetting) {
if (!baseURL || !apiKey || !modelId || !baseURL.startsWith("https://")) {
return {
models: [],
publicModels: [],
defaultModelId: null,
issues: ["legacy_compatible_incomplete"],
};
}
const model: ResolvedLanguageModel = {
id: "legacy-compatible",
label: modelId,
description: "当前默认模型",
creditCost: 1,
isDefault: true,
mode: "compatible",
model: {
providerId: environmentValue(environment, "LLM_PROVIDER_ID") || "third-party",
modelId,
url: baseURL,
apiKey,
},
};
return {
models: [model],
publicModels: [publicModel(model)],
defaultModelId: model.id,
issues: [],
};
}
const openAIKey = environmentValue(environment, "OPENAI_API_KEY");
if (!openAIKey) {
return { models: [], publicModels: [], defaultModelId: null, issues: ["model_not_configured"] };
}
const modelIdValue = environmentValue(environment, "MASTRA_MODEL") || "openai/gpt-5-mini";
const model: ResolvedLanguageModel = {
id: "legacy-openai",
label: modelIdValue.replace(/^openai\//, ""),
description: "当前默认模型",
creditCost: 1,
isDefault: true,
mode: "openai",
model: modelIdValue,
};
return {
models: [model],
publicModels: [publicModel(model)],
defaultModelId: model.id,
issues: [],
};
}
export function resolveLanguageModelCatalog(environment: Environment): LanguageModelCatalog {
const rawCatalog = environmentValue(environment, "LLM_MODELS_JSON");
return rawCatalog
? resolveExplicitCatalog(environment, rawCatalog)
: resolveLegacyCatalog(environment);
}
export const languageModelCatalog = resolveLanguageModelCatalog(process.env);
export function resolveLanguageModelFromCatalog(catalog: LanguageModelCatalog, modelId: string) {
return catalog.models.find((model) => model.id === modelId) ?? null;
}
export function resolveLanguageModel(modelId: string) {
return resolveLanguageModelFromCatalog(languageModelCatalog, modelId);
}
export function defaultLanguageModel() {
const defaultModelId = languageModelCatalog.defaultModelId;
return defaultModelId ? resolveLanguageModel(defaultModelId) : null;
}
export function publicLanguageModelCatalog() {
return {
models: languageModelCatalog.publicModels,
defaultModelId: languageModelCatalog.defaultModelId,
};
}
const configuredDefaultModel = defaultLanguageModel();
export const languageModelSettings = {
mode: configuredDefaultModel?.mode ?? "openai",
model: configuredDefaultModel?.model ?? "openai/gpt-5-mini",
configured: Boolean(configuredDefaultModel),
missing: languageModelCatalog.issues,
} as const;
export function languageModelConfigurationMessage() {
return configuredDefaultModel ? null : "未配置可用的语言模型";
}