feat: route consultations by model
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
consultationInputSchema,
|
||||
jyotishAgent,
|
||||
runConsultationWorkflow,
|
||||
getJyotishAgent,
|
||||
} from "@/mastra";
|
||||
import {
|
||||
languageModelConfigurationMessage,
|
||||
languageModelSettings,
|
||||
resolveLanguageModel,
|
||||
} from "@/mastra/model";
|
||||
import { blocksPromptExtraction } from "@/lib/consult-safety";
|
||||
import { CreditRpcError, runCreditRpc } from "@/lib/consultation-billing";
|
||||
@@ -20,6 +19,7 @@ export const maxDuration = 60;
|
||||
|
||||
const chatRequestSchema = consultationInputSchema.extend({
|
||||
requestId: z.string().uuid(),
|
||||
modelId: z.string().trim().min(1).max(64),
|
||||
name: z.string().trim().max(80).optional().default(""),
|
||||
history: z.array(z.object({
|
||||
role: z.enum(["user", "assistant"]),
|
||||
@@ -35,42 +35,11 @@ function currentTimeContext(now = new Date()) {
|
||||
return `服务端当前时间(权威):${now.toISOString()};中国标准时间(UTC+8):${chinaTime}。涉及“现在、今天、今年、未来几个月”等相对时间时,以此为准。`;
|
||||
}
|
||||
|
||||
async function* staticTextStream(text: string) {
|
||||
yield text;
|
||||
}
|
||||
|
||||
function engineSummary(data: Record<string, unknown>) {
|
||||
const topics = Array.isArray(data.guided_topics) ? data.guided_topics : [];
|
||||
const routing = data.routing && typeof data.routing === "object" ? data.routing : {};
|
||||
const route = "primary_route" in routing ? String(routing.primary_route) : "统一咨询工作流";
|
||||
const topicText = topics
|
||||
.slice(0, 3)
|
||||
.map((item) => {
|
||||
if (!item || typeof item !== "object") return null;
|
||||
const record = item as Record<string, unknown>;
|
||||
return String(record.title || record.label || record.theme || "值得继续探索的主题");
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return [
|
||||
"星盘计算已完成,但当前没有配置 AI 模型,因此先返回引擎摘要。",
|
||||
`本次路由:${route}。`,
|
||||
topicText.length ? `建议继续查看:${topicText.join("、")}。` : "可继续查看事业、关系与年度时间窗口。",
|
||||
"启动 AI 解读需配置模型;原始计算结果已保留。",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function configuredModelId() {
|
||||
if (languageModelSettings.mode === "compatible") {
|
||||
return process.env.LLM_MODEL?.trim() || "third-party";
|
||||
}
|
||||
return process.env.MASTRA_MODEL?.trim() || "openai/gpt-5-mini";
|
||||
}
|
||||
|
||||
async function recordModelUsage(
|
||||
accounting: ReturnType<typeof createAdminSupabaseClient>,
|
||||
userId: string,
|
||||
requestId: string,
|
||||
modelId: string,
|
||||
usage: Promise<{ inputTokens?: number; outputTokens?: number }>,
|
||||
) {
|
||||
try {
|
||||
@@ -78,7 +47,7 @@ async function recordModelUsage(
|
||||
const { error } = await accounting
|
||||
.from("credit_transactions")
|
||||
.update({
|
||||
model: configuredModelId(),
|
||||
model: modelId,
|
||||
input_tokens: Math.max(0, Math.trunc(resolved.inputTokens ?? 0)),
|
||||
output_tokens: Math.max(0, Math.trunc(resolved.outputTokens ?? 0)),
|
||||
})
|
||||
@@ -134,6 +103,14 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
const selectedModel = resolveLanguageModel(parsed.data.modelId);
|
||||
if (!selectedModel) {
|
||||
return NextResponse.json(
|
||||
{ error: "模型暂不可用", message: "请选择其他模型后重新发送,本次不会扣除点数。" },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
const userId = user.id;
|
||||
const requestId = parsed.data.requestId;
|
||||
let reserveResult;
|
||||
@@ -181,18 +158,7 @@ export async function POST(request: Request) {
|
||||
const { history, name } = parsed.data;
|
||||
const toolInput = consultationInputSchema.parse(parsed.data);
|
||||
|
||||
if (!languageModelSettings.configured) {
|
||||
const evidence = await runConsultationWorkflow(toolInput);
|
||||
return streamTextResponse(staticTextStream(engineSummary(evidence)), {
|
||||
mode: "engine",
|
||||
requestId,
|
||||
onComplete: () => settle(complete),
|
||||
onError: (_error, emitted) => settle(emitted ? complete : cancel),
|
||||
onCancel: (emitted) => settle(emitted ? complete : cancel),
|
||||
});
|
||||
}
|
||||
|
||||
const result = await jyotishAgent.stream([
|
||||
const result = await getJyotishAgent(selectedModel).stream([
|
||||
...history.map((message) => message.role === "user"
|
||||
? { role: "user" as const, content: message.text }
|
||||
: { role: "assistant" as const, content: message.text }),
|
||||
@@ -209,7 +175,7 @@ export async function POST(request: Request) {
|
||||
]);
|
||||
const completeAndRecordUsage = async () => {
|
||||
await complete();
|
||||
void recordModelUsage(accounting, userId, requestId, result.totalUsage);
|
||||
void recordModelUsage(accounting, userId, requestId, selectedModel.id, result.totalUsage);
|
||||
};
|
||||
const settleInterrupted = (emitted: boolean) => settle(emitted ? completeAndRecordUsage : cancel);
|
||||
return streamTextResponse(result.textStream, {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { publicLanguageModelCatalog } from "@/mastra/model";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET() {
|
||||
let supabase: Awaited<ReturnType<typeof createServerSupabaseClient>>;
|
||||
try {
|
||||
supabase = await createServerSupabaseClient();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json(
|
||||
{ error: "服务尚未配置", message: "请先配置 Supabase 环境变量。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||||
if (authError || !user) {
|
||||
return NextResponse.json(
|
||||
{ error: "请先登录", message: "登录后才能读取可用模型。" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const catalog = publicLanguageModelCatalog();
|
||||
if (!catalog.defaultModelId || catalog.models.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "模型服务尚未配置", message: "当前没有可用的咨询模型。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(catalog);
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { onboardingAgent } from "@/mastra";
|
||||
import { languageModelSettings } from "@/mastra/model";
|
||||
import { getOnboardingAgent } from "@/mastra";
|
||||
import { defaultLanguageModel } from "@/mastra/model";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const maxDuration = 30;
|
||||
@@ -134,9 +134,10 @@ export async function POST() {
|
||||
let payload = fallbackPayload;
|
||||
let source: "agent" | "fallback" = "fallback";
|
||||
|
||||
if (languageModelSettings.configured) {
|
||||
const onboardingModel = defaultLanguageModel();
|
||||
if (onboardingModel) {
|
||||
try {
|
||||
const result = await onboardingAgent.generate([
|
||||
const result = await getOnboardingAgent(onboardingModel).generate([
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
||||
Reference in New Issue
Block a user