feat(chat): expose model options config
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
export type SupportedModel = string;
|
||||
export type AgentModelIcon = "bolt" | "sparkle";
|
||||
|
||||
export type AgentModelOption = {
|
||||
id: SupportedModel;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: AgentModelIcon;
|
||||
};
|
||||
|
||||
export const defaultAgentModelOptions: AgentModelOption[] = [
|
||||
{
|
||||
id: "deepseek/deepseek-v4-flash",
|
||||
label: "快速",
|
||||
description: "快速回答和任务执行",
|
||||
icon: "bolt",
|
||||
},
|
||||
{
|
||||
id: "deepseek/deepseek-v4-pro",
|
||||
label: "专家",
|
||||
description: "探索、解决复杂任务",
|
||||
icon: "sparkle",
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultAgentModelOptionsJson = JSON.stringify(defaultAgentModelOptions);
|
||||
|
||||
const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
export const parseAgentModelOptions = (value: string): AgentModelOption[] => {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch {
|
||||
throw new Error("OPENCODE_MODEL_OPTIONS must be valid JSON");
|
||||
}
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error("OPENCODE_MODEL_OPTIONS must be a JSON array");
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
return parsed.map((item, index) => {
|
||||
if (!isObjectRecord(item)) {
|
||||
throw new Error(`OPENCODE_MODEL_OPTIONS[${index}] must be an object`);
|
||||
}
|
||||
const id = typeof item.id === "string" ? item.id.trim() : "";
|
||||
if (!id) {
|
||||
throw new Error(`OPENCODE_MODEL_OPTIONS[${index}].id is required`);
|
||||
}
|
||||
if (seen.has(id)) {
|
||||
throw new Error(`duplicate OPENCODE_MODEL_OPTIONS id: ${id}`);
|
||||
}
|
||||
seen.add(id);
|
||||
|
||||
const label =
|
||||
typeof item.label === "string" && item.label.trim()
|
||||
? item.label.trim()
|
||||
: id;
|
||||
const description =
|
||||
typeof item.description === "string" && item.description.trim()
|
||||
? item.description.trim()
|
||||
: label;
|
||||
const icon = item.icon === "bolt" || item.icon === "sparkle"
|
||||
? item.icon
|
||||
: "sparkle";
|
||||
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
description,
|
||||
icon,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const getAgentModelIds = (options: AgentModelOption[]) =>
|
||||
options.map((option) => option.id);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { config } from "../config.js";
|
||||
import {
|
||||
getAgentModelIds,
|
||||
parseAgentModelOptions,
|
||||
type SupportedModel,
|
||||
} from "./modelConfig.js";
|
||||
|
||||
export {
|
||||
type AgentModelIcon,
|
||||
type AgentModelOption,
|
||||
type SupportedModel,
|
||||
} from "./modelConfig.js";
|
||||
|
||||
export const agentModelOptions = parseAgentModelOptions(
|
||||
config.OPENCODE_MODEL_OPTIONS,
|
||||
);
|
||||
|
||||
export const supportedModels = getAgentModelIds(agentModelOptions);
|
||||
|
||||
export const isSupportedModel = (model: string): model is SupportedModel =>
|
||||
supportedModels.includes(model);
|
||||
|
||||
export const resolveDefaultModel = (model: string): SupportedModel => {
|
||||
if (!isSupportedModel(model)) {
|
||||
throw new Error(`unsupported default agent model: ${model}`);
|
||||
}
|
||||
return model;
|
||||
};
|
||||
@@ -1,6 +1,12 @@
|
||||
import dotenv from "dotenv";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
defaultAgentModelOptionsJson,
|
||||
getAgentModelIds,
|
||||
parseAgentModelOptions,
|
||||
} from "./chat/modelConfig.js";
|
||||
|
||||
// 本地开发可在项目根目录放 .local.env;已存在的系统环境变量优先级更高。
|
||||
dotenv.config({ path: ".local.env", override: false });
|
||||
|
||||
@@ -45,6 +51,8 @@ const envSchema = z
|
||||
OPENCODE_TIMEOUT_MS: z.coerce.number().int().positive().default(5000),
|
||||
// 默认使用的 opencode 模型标识。
|
||||
OPENCODE_MODEL: z.string().default("deepseek/deepseek-v4-flash"),
|
||||
// 聊天 UI 和 /stream 允许选择的 opencode 模型完整配置,JSON 数组。
|
||||
OPENCODE_MODEL_OPTIONS: z.string().default(defaultAgentModelOptionsJson),
|
||||
// opencode skills 树目录;会在运行时解析为绝对路径,避免工具 cwd 偏移。
|
||||
OPENCODE_SKILLS_ROOT_DIR: z.string().default("./.opencode/skills"),
|
||||
// client 模式下,目标 opencode server 的基础地址。
|
||||
@@ -116,6 +124,32 @@ const envSchema = z
|
||||
message: "OPENCODE_CLIENT_BASE_URL is required when OPENCODE_MODE=client",
|
||||
});
|
||||
}
|
||||
let modelOptions;
|
||||
try {
|
||||
modelOptions = parseAgentModelOptions(env.OPENCODE_MODEL_OPTIONS);
|
||||
} catch (error) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["OPENCODE_MODEL_OPTIONS"],
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const supportedModels = getAgentModelIds(modelOptions);
|
||||
if (supportedModels.length === 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["OPENCODE_MODEL_OPTIONS"],
|
||||
message: "OPENCODE_MODEL_OPTIONS must include at least one model",
|
||||
});
|
||||
}
|
||||
if (!supportedModels.includes(env.OPENCODE_MODEL)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["OPENCODE_MODEL"],
|
||||
message: "OPENCODE_MODEL must be included in OPENCODE_MODEL_OPTIONS",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type AppConfig = z.infer<typeof envSchema>;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { writeLearningAuditLog } from "../audit/learningAudit.js";
|
||||
import { type SupportedModel } from "../chat/models.js";
|
||||
import { type ChatRequestContext } from "../chat/sessionBridge.js";
|
||||
import { config } from "../config.js";
|
||||
import { type SessionTurnRecord, SessionTranscriptStore } from "../sessions/transcriptStore.js";
|
||||
@@ -64,8 +65,6 @@ const reviewResultSchema = z.object({
|
||||
type GateResult = z.infer<typeof gateResultSchema>;
|
||||
type ReviewResult = z.infer<typeof reviewResultSchema>;
|
||||
|
||||
type SupportedModel = "deepseek/deepseek-v4-flash" | "deepseek/deepseek-v4-pro";
|
||||
|
||||
type TurnReviewInput = {
|
||||
assistantMessage: string;
|
||||
model?: SupportedModel;
|
||||
|
||||
+17
-3
@@ -1,6 +1,13 @@
|
||||
import { Router } from "express";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
agentModelOptions,
|
||||
isSupportedModel,
|
||||
resolveDefaultModel,
|
||||
type SupportedModel,
|
||||
} from "../chat/models.js";
|
||||
import { config } from "../config.js";
|
||||
import { type LearningOrchestrator } from "../learning/orchestrator.js";
|
||||
import { type SessionTranscriptStore } from "../sessions/transcriptStore.js";
|
||||
import { logger } from "../logger.js";
|
||||
@@ -28,8 +35,6 @@ import {
|
||||
type PermissionRequestPayload,
|
||||
type QuestionRequestPayload,
|
||||
streamPromptResponse,
|
||||
supportedModels,
|
||||
type SupportedModel,
|
||||
type TodoUpdatePayload,
|
||||
} from "./chatStream.js";
|
||||
import {
|
||||
@@ -54,7 +59,9 @@ import {
|
||||
const payloadSchema = z.object({
|
||||
message: z.string().min(1).max(10000),
|
||||
session_id: z.string().max(128).optional(),
|
||||
model: z.enum(supportedModels).optional(),
|
||||
model: z.string().refine(isSupportedModel, {
|
||||
message: "unsupported model",
|
||||
}).optional(),
|
||||
approval_mode: z.enum(["request", "always"]).optional().default("request"),
|
||||
});
|
||||
|
||||
@@ -115,6 +122,13 @@ export const buildChatRouter = (
|
||||
) => {
|
||||
const chatRouter = Router();
|
||||
|
||||
chatRouter.get("/models", (_req, res) => {
|
||||
res.json({
|
||||
default_model: resolveDefaultModel(config.OPENCODE_MODEL),
|
||||
models: agentModelOptions,
|
||||
});
|
||||
});
|
||||
|
||||
chatRouter.post("/session", async (req, res) => {
|
||||
const parsed = createSessionPayloadSchema.safeParse(req.body ?? {});
|
||||
if (!parsed.success) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Event as OpencodeEvent, Part } from "@opencode-ai/sdk/v2";
|
||||
|
||||
import { writeLlmRequestAuditLog } from "../audit/llmRequestAudit.js";
|
||||
import { type SupportedModel } from "../chat/models.js";
|
||||
import { logger } from "../logger.js";
|
||||
import {
|
||||
type PermissionReply,
|
||||
@@ -54,12 +55,6 @@ export {
|
||||
type TodoUpdatePayload,
|
||||
} from "./chatStreamEvents.js";
|
||||
|
||||
export const supportedModels = [
|
||||
"deepseek/deepseek-v4-flash",
|
||||
"deepseek/deepseek-v4-pro",
|
||||
] as const;
|
||||
|
||||
export type SupportedModel = (typeof supportedModels)[number];
|
||||
export type ApprovalMode = "request" | "always";
|
||||
|
||||
type StreamPromptOptions = {
|
||||
|
||||
Reference in New Issue
Block a user