feat: add server model catalog
This commit is contained in:
+13
-7
@@ -72,14 +72,20 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=...
|
||||
SUPABASE_SERVICE_ROLE_KEY=...
|
||||
ADMIN_EMAILS=...
|
||||
|
||||
# Either OpenAI:
|
||||
OPENAI_API_KEY=...
|
||||
MASTRA_MODEL=...
|
||||
# Recommended multi-model catalog. The JSON references server-only keys.
|
||||
LLM_DEFAULT_MODEL_ID=deepseek-pro
|
||||
LLM_MODELS_JSON='[{"id":"deepseek-pro","label":"DeepSeek V4 Pro","description":"更适合复杂分析","provider":"openai-compatible","baseURL":"https://api.deepseek.com","apiKeyEnv":"DEEPSEEK_API_KEY","model":"deepseek-v4-pro","creditCost":1},{"id":"gpt-5-mini","label":"ChatGPT 5 Mini","description":"响应稳定、速度均衡","provider":"openai","apiKeyEnv":"OPENAI_API_KEY","model":"openai/gpt-5-mini","creditCost":1}]'
|
||||
DEEPSEEK_API_KEY=<server-secret>
|
||||
OPENAI_API_KEY=<server-secret>
|
||||
|
||||
# Or an OpenAI-compatible provider:
|
||||
LLM_BASE_URL=...
|
||||
LLM_API_KEY=...
|
||||
LLM_MODEL=...
|
||||
# Legacy single-model OpenAI configuration remains supported:
|
||||
# OPENAI_API_KEY=<server-secret>
|
||||
# MASTRA_MODEL=openai/gpt-5-mini
|
||||
|
||||
# Legacy single OpenAI-compatible provider remains supported:
|
||||
# LLM_BASE_URL=https://provider.example/v1
|
||||
# LLM_API_KEY=<server-secret>
|
||||
# LLM_MODEL=provider-model-id
|
||||
|
||||
# Optional VedAstro official upstream; local fallback remains available:
|
||||
VEDASTRO_API_ENDPOINT=...
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
**Files:**
|
||||
- Modify: `frontend/src/mastra/model.ts`
|
||||
- Create: `frontend/tests/model-catalog.test.ts`
|
||||
- Modify: `frontend/.env.example`
|
||||
- Modify: `frontend/README.md`
|
||||
- Modify: `deploy/README.md`
|
||||
|
||||
@@ -132,12 +131,12 @@ Expected: all catalog tests PASS and TypeScript exits `0`.
|
||||
|
||||
- [ ] **Step 5: Document configuration**
|
||||
|
||||
Update `.env.example`, `frontend/README.md`, and `deploy/README.md` with `LLM_MODELS_JSON`, `LLM_DEFAULT_MODEL_ID`, one secret environment variable per provider, and the existing single-model fallback. Use redacted values only.
|
||||
Update `frontend/README.md` and `deploy/README.md` with `LLM_MODELS_JSON`, `LLM_DEFAULT_MODEL_ID`, one secret environment variable per provider, and the existing single-model fallback. Use redacted values only. Do not add `frontend/.env.example`: the repository intentionally ignores all `.env*` files.
|
||||
|
||||
- [ ] **Step 6: Commit the catalog task**
|
||||
|
||||
```bash
|
||||
git add frontend/src/mastra/model.ts frontend/tests/model-catalog.test.ts frontend/.env.example frontend/README.md deploy/README.md
|
||||
git add frontend/src/mastra/model.ts frontend/tests/model-catalog.test.ts frontend/README.md deploy/README.md docs/superpowers/plans/2026-07-17-multi-model-chat-selection.md
|
||||
git commit -m "feat: add server model catalog"
|
||||
```
|
||||
|
||||
|
||||
+15
-12
@@ -39,24 +39,27 @@ cp .env.example .env.local
|
||||
# Python 占星计算服务
|
||||
JYOTISH_API_BASE=http://127.0.0.1:5200
|
||||
|
||||
# 方案 A:默认 OpenAI
|
||||
OPENAI_API_KEY=sk-...
|
||||
MASTRA_MODEL=openai/gpt-5-mini
|
||||
# 推荐:多模型目录。目录只保存路由元数据,Key 由 apiKeyEnv 引用。
|
||||
LLM_DEFAULT_MODEL_ID=deepseek-pro
|
||||
LLM_MODELS_JSON='[{"id":"deepseek-pro","label":"DeepSeek V4 Pro","description":"更适合复杂分析","provider":"openai-compatible","baseURL":"https://api.deepseek.com","apiKeyEnv":"DEEPSEEK_API_KEY","model":"deepseek-v4-pro","creditCost":1},{"id":"gpt-5-mini","label":"ChatGPT 5 Mini","description":"响应稳定、速度均衡","provider":"openai","apiKeyEnv":"OPENAI_API_KEY","model":"openai/gpt-5-mini","creditCost":1}]'
|
||||
DEEPSEEK_API_KEY=<server-secret>
|
||||
OPENAI_API_KEY=<server-secret>
|
||||
|
||||
# 方案 B:任意 OpenAI-compatible 第三方模型
|
||||
# 只要填写任一 LLM_* 项,应用便会优先使用本方案。
|
||||
# Base URL 通常填写到 /v1,不要填写完整 /chat/completions 地址。
|
||||
LLM_BASE_URL=https://your-provider.example/v1
|
||||
LLM_API_KEY=your-secret-key
|
||||
LLM_MODEL=your-model-id
|
||||
# 可选:只作为 Mastra 内部标签,不影响请求地址
|
||||
LLM_PROVIDER_ID=third-party
|
||||
# 兼容旧的单模型 OpenAI 配置
|
||||
# OPENAI_API_KEY=<server-secret>
|
||||
# MASTRA_MODEL=openai/gpt-5-mini
|
||||
|
||||
# 兼容旧的单个 OpenAI-compatible 配置
|
||||
# LLM_BASE_URL=https://your-provider.example/v1
|
||||
# LLM_API_KEY=<server-secret>
|
||||
# LLM_MODEL=your-model-id
|
||||
# LLM_PROVIDER_ID=third-party
|
||||
|
||||
# 可选:部署目录与本仓结构不同时,显式指定 Mastra Skill 目录
|
||||
# JYOTISH_SKILL_PATH=/absolute/path/to/yinduzhanxing/skills/jyotish-vedic-astrology
|
||||
```
|
||||
|
||||
第三方端点必须兼容 OpenAI 的 Chat Completions 调用方式,并支持工具调用(function calling),否则 Agent 无法稳定调用占星计算工具。密钥只放在 `.env.local`,**不要**加 `NEXT_PUBLIC_` 前缀,也不要提交到 Git。每次修改 `.env.local` 后重启 Next.js 开发服务器。
|
||||
第三方端点必须兼容 OpenAI 的 Chat Completions 调用方式,并支持工具调用(function calling),否则 Agent 无法稳定调用占星计算工具。`LLM_MODELS_JSON` 只能填写服务端认可的固定地址和模型;浏览器只会得到模型 ID、名称、说明和点数。密钥只放在 `.env.local`,**不要**加 `NEXT_PUBLIC_` 前缀,也不要提交到 Git。每次修改 `.env.local` 后重启 Next.js 开发服务器。
|
||||
|
||||
## Skill 如何触发
|
||||
|
||||
|
||||
+227
-52
@@ -1,69 +1,244 @@
|
||||
import type { MastraModelConfig } from "@mastra/core/llm";
|
||||
import { z } from "zod";
|
||||
|
||||
type Environment = Readonly<Record<string, string | undefined>>;
|
||||
type LanguageModelMode = "openai" | "compatible";
|
||||
|
||||
type LanguageModelSettings = {
|
||||
mode: LanguageModelMode;
|
||||
model: MastraModelConfig;
|
||||
configured: boolean;
|
||||
missing: string[];
|
||||
export type PublicLanguageModel = {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly description: string;
|
||||
readonly creditCost: 1;
|
||||
readonly isDefault: boolean;
|
||||
};
|
||||
|
||||
function environmentValue(name: string) {
|
||||
return process.env[name]?.trim() ?? "";
|
||||
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() ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves either the default OpenAI model or a third-party endpoint that
|
||||
* implements the OpenAI Chat Completions API. A supplied LLM_* value switches
|
||||
* the app to compatible-provider mode, so the old OPENAI_* setup remains
|
||||
* backwards compatible.
|
||||
*/
|
||||
function resolveLanguageModelSettings(): LanguageModelSettings {
|
||||
const baseURL = environmentValue("LLM_BASE_URL");
|
||||
const apiKey = environmentValue("LLM_API_KEY");
|
||||
const modelId = environmentValue("LLM_MODEL");
|
||||
const hasCompatibleSetting = Boolean(baseURL || apiKey || modelId);
|
||||
|
||||
if (hasCompatibleSetting) {
|
||||
const missing = [
|
||||
!baseURL && "LLM_BASE_URL",
|
||||
!apiKey && "LLM_API_KEY",
|
||||
!modelId && "LLM_MODEL",
|
||||
].filter((value): value is string => Boolean(value));
|
||||
|
||||
return {
|
||||
mode: "compatible",
|
||||
configured: missing.length === 0,
|
||||
missing,
|
||||
model: {
|
||||
// This is an internal label for Mastra. It does not need to match the
|
||||
// provider's company name; the URL determines the actual endpoint.
|
||||
providerId: environmentValue("LLM_PROVIDER_ID") || "third-party",
|
||||
modelId: modelId || "not-configured",
|
||||
url: baseURL || undefined,
|
||||
apiKey: apiKey || undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const openAIKey = environmentValue("OPENAI_API_KEY");
|
||||
function publicModel(model: ResolvedLanguageModel): PublicLanguageModel {
|
||||
return {
|
||||
mode: "openai",
|
||||
configured: Boolean(openAIKey),
|
||||
missing: openAIKey ? [] : ["OPENAI_API_KEY"],
|
||||
model: environmentValue("MASTRA_MODEL") || "openai/gpt-5-mini",
|
||||
id: model.id,
|
||||
label: model.label,
|
||||
description: model.description,
|
||||
creditCost: model.creditCost,
|
||||
isDefault: model.isDefault,
|
||||
};
|
||||
}
|
||||
|
||||
export const languageModelSettings = resolveLanguageModelSettings();
|
||||
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;
|
||||
|
||||
export function languageModelConfigurationMessage() {
|
||||
if (languageModelSettings.configured) return null;
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (languageModelSettings.mode === "compatible") {
|
||||
return `第三方模型配置不完整:${languageModelSettings.missing.join("、")}`;
|
||||
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"] };
|
||||
}
|
||||
|
||||
return "未配置 OPENAI_API_KEY";
|
||||
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 resolveLanguageModel(modelId: string) {
|
||||
return languageModelCatalog.models.find((model) => model.id === modelId) ?? null;
|
||||
}
|
||||
|
||||
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 : "未配置可用的语言模型";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { resolveLanguageModelCatalog } from "../src/mastra/model.ts";
|
||||
|
||||
const configuredModels = [
|
||||
{
|
||||
id: "deepseek-pro",
|
||||
label: "DeepSeek V4 Pro",
|
||||
description: "复杂分析",
|
||||
provider: "openai-compatible",
|
||||
baseURL: "https://api.deepseek.com",
|
||||
apiKeyEnv: "DEEPSEEK_API_KEY",
|
||||
model: "deepseek-v4-pro",
|
||||
creditCost: 1,
|
||||
},
|
||||
{
|
||||
id: "gpt-mini",
|
||||
label: "ChatGPT Mini",
|
||||
description: "均衡响应",
|
||||
provider: "openai",
|
||||
apiKeyEnv: "OPENAI_API_KEY",
|
||||
model: "openai/gpt-5-mini",
|
||||
creditCost: 1,
|
||||
},
|
||||
] as const;
|
||||
|
||||
test("resolves configured models while returning sanitized public metadata", () => {
|
||||
// Given
|
||||
const environment = {
|
||||
LLM_DEFAULT_MODEL_ID: "deepseek-pro",
|
||||
LLM_MODELS_JSON: JSON.stringify(configuredModels),
|
||||
DEEPSEEK_API_KEY: "deepseek-secret",
|
||||
OPENAI_API_KEY: "openai-secret",
|
||||
};
|
||||
|
||||
// When
|
||||
const catalog = resolveLanguageModelCatalog(environment);
|
||||
|
||||
// Then
|
||||
assert.equal(catalog.defaultModelId, "deepseek-pro");
|
||||
assert.deepEqual(catalog.publicModels[0], {
|
||||
id: "deepseek-pro",
|
||||
label: "DeepSeek V4 Pro",
|
||||
description: "复杂分析",
|
||||
creditCost: 1,
|
||||
isDefault: true,
|
||||
});
|
||||
assert.equal(JSON.stringify(catalog.publicModels).includes("secret"), false);
|
||||
assert.equal(JSON.stringify(catalog.publicModels).includes("baseURL"), false);
|
||||
assert.equal(catalog.models[1]?.model, "openai/gpt-5-mini");
|
||||
});
|
||||
|
||||
test("excludes an invalid catalog entry without leaking its secret", () => {
|
||||
// Given
|
||||
const environment = {
|
||||
LLM_DEFAULT_MODEL_ID: "gpt-mini",
|
||||
LLM_MODELS_JSON: JSON.stringify([
|
||||
configuredModels[1],
|
||||
{
|
||||
...configuredModels[0],
|
||||
id: "broken model",
|
||||
baseURL: "http://api.deepseek.com",
|
||||
},
|
||||
]),
|
||||
OPENAI_API_KEY: "openai-secret",
|
||||
DEEPSEEK_API_KEY: "must-not-appear",
|
||||
};
|
||||
|
||||
// When
|
||||
const catalog = resolveLanguageModelCatalog(environment);
|
||||
|
||||
// Then
|
||||
assert.deepEqual(catalog.models.map((model) => model.id), ["gpt-mini"]);
|
||||
assert.equal(catalog.issues.length, 1);
|
||||
assert.equal(JSON.stringify(catalog.issues).includes("must-not-appear"), false);
|
||||
});
|
||||
|
||||
test("does not choose an undeclared default model", () => {
|
||||
// Given
|
||||
const environment = {
|
||||
LLM_DEFAULT_MODEL_ID: "removed-model",
|
||||
LLM_MODELS_JSON: JSON.stringify([configuredModels[1]]),
|
||||
OPENAI_API_KEY: "openai-secret",
|
||||
};
|
||||
|
||||
// When
|
||||
const catalog = resolveLanguageModelCatalog(environment);
|
||||
|
||||
// Then
|
||||
assert.equal(catalog.defaultModelId, null);
|
||||
assert.equal(catalog.issues.includes("default_model_unavailable"), true);
|
||||
});
|
||||
|
||||
test("derives the shipped compatible-provider configuration when no catalog exists", () => {
|
||||
// Given
|
||||
const environment = {
|
||||
LLM_BASE_URL: "https://api.deepseek.com",
|
||||
LLM_API_KEY: "legacy-secret",
|
||||
LLM_MODEL: "deepseek-v4-pro",
|
||||
LLM_PROVIDER_ID: "deepseek",
|
||||
};
|
||||
|
||||
// When
|
||||
const catalog = resolveLanguageModelCatalog(environment);
|
||||
|
||||
// Then
|
||||
assert.equal(catalog.defaultModelId, "legacy-compatible");
|
||||
assert.equal(catalog.models[0]?.id, "legacy-compatible");
|
||||
assert.equal(catalog.publicModels[0]?.label, "deepseek-v4-pro");
|
||||
assert.equal(JSON.stringify(catalog.publicModels).includes("legacy-secret"), false);
|
||||
});
|
||||
|
||||
test("reports an incomplete legacy provider without inventing a model", () => {
|
||||
// Given
|
||||
const environment = {
|
||||
LLM_BASE_URL: "https://api.deepseek.com",
|
||||
LLM_MODEL: "deepseek-v4-pro",
|
||||
};
|
||||
|
||||
// When
|
||||
const catalog = resolveLanguageModelCatalog(environment);
|
||||
|
||||
// Then
|
||||
assert.equal(catalog.models.length, 0);
|
||||
assert.equal(catalog.defaultModelId, null);
|
||||
assert.equal(catalog.issues.includes("legacy_compatible_incomplete"), true);
|
||||
});
|
||||
Reference in New Issue
Block a user