Commit f9716ee1 by luoqi

chore(ai): normalize DeepSeek Flash model and pricing

parent 6e099cc1
...@@ -153,7 +153,7 @@ icon: Sparkles ...@@ -153,7 +153,7 @@ icon: Sparkles
| **禁词 + 不承诺** | 不许"保证治好" / 报价 / 写死时间(诊所没排班接口,时间只能给"示例") | | **禁词 + 不承诺** | 不许"保证治好" / 报价 / 写死时间(诊所没排班接口,时间只能给"示例") |
| **失败降级** | 大模型出错或违规 → 退回安全模板,客服永远有东西可用 | | **失败降级** | 大模型出错或违规 → 退回安全模板,客服永远有东西可用 |
> 📐 **真理源**:提示词组装 `skill-composer.ts` + `skills/*/SKILL.md`(按 frontmatter 声明式匹配);安全规则**单一源** `safety-rules.ts`(机器闸 + prompt 同引,输出后扫禁词 / 加粗时间 / 临床码,命中即兜底);模型默认 `deepseek-v4-flash`;每次落 `agent_invocations`(档位 / 模型 / token / cost / 用了哪些块)。话术块是 MD 文件,**运营可改文案不动代码**。 > 📐 **真理源**:提示词组装 `skill-composer.ts` + `skills/*/SKILL.md`(按 frontmatter 声明式匹配);安全规则**单一源** `safety-rules.ts`(机器闸 + prompt 同引,输出后扫禁词 / 加粗时间 / 临床码,命中即兜底);模型默认 `deepseek-flash`;每次落 `agent_invocations`(档位 / 模型 / token / cost / 用了哪些块)。话术块是 MD 文件,**运营可改文案不动代码**。
> **本质**:话术不是"一个大模板",而是按患者特征拼装 + 大模型润色 + 安全兜底。 > **本质**:话术不是"一个大模板",而是按患者特征拼装 + 大模型润色 + 安全兜底。
......
...@@ -79,7 +79,7 @@ system prompt = `base common`(`shared/skills/_base/common.md`,三档共用:角 ...@@ -79,7 +79,7 @@ system prompt = `base common`(`shared/skills/_base/common.md`,三档共用:角
## provider 与落账 ## provider 与落账
- **模型**:三档默认 `deepseek-v4-flash`(`?model=` 可覆盖)。 - **模型**:三档默认 `deepseek-flash`(`?model=` 可覆盖)。
- **落账**:每次经 `AiCallRunner` → `invocation-recorder` 两段式落 `agent_invocations`(tier 嵌在 `promptVersion`、`modelProvider`/`modelName`、token、`costYuan`、source);深度档额外 `attachJudge` 落自检质量分。 - **落账**:每次经 `AiCallRunner` → `invocation-recorder` 两段式落 `agent_invocations`(tier 嵌在 `promptVersion`、`modelProvider`/`modelName`、token、`costYuan`、source);深度档额外 `attachJudge` 落自检质量分。
- **写库**:`plan_scripts` upsert(`content` Markdown、`status='ready'`、`source`、`agentInvocationId`)。 - **写库**:`plan_scripts` upsert(`content` Markdown、`status='ready'`、`source`、`agentInvocationId`)。
......
...@@ -85,7 +85,8 @@ DW_CLICKHOUSE_PASSWORD=<密码> # env 名须等于 manifest 的 ...@@ -85,7 +85,8 @@ DW_CLICKHOUSE_PASSWORD=<密码> # env 名须等于 manifest 的
# ─── AI(DeepSeek)─── # ─── AI(DeepSeek)───
DEEPSEEK_API_KEY=<key> DEEPSEEK_API_KEY=<key>
DEEPSEEK_BASE_URL=https://api.deepseek.com DEEPSEEK_BASE_URL=https://api.deepseek.com
AI_DEFAULT_MODEL=deepseek-v4-flash # 旧 deepseek-v4-flash 配置兼容归一;Flash 成本按高峰价估算,非实际账单
AI_DEFAULT_MODEL=deepseek-flash
# ─── 定时任务 ─── # ─── 定时任务 ───
# 注:增量 / 滞后监控 / 兜底扫描的代码默认是"永不"哨兵,必须显式设 env 才会调度 # 注:增量 / 滞后监控 / 兜底扫描的代码默认是"永不"哨兵,必须显式设 env 才会调度
......
...@@ -84,10 +84,10 @@ DEEPSEEK_API_KEY= ...@@ -84,10 +84,10 @@ DEEPSEEK_API_KEY=
DEEPSEEK_BASE_URL=https://api.deepseek.com DEEPSEEK_BASE_URL=https://api.deepseek.com
# 默认主力模型 # 默认主力模型
# local: deepseek-v4-flash (省钱开发) # local: deepseek-flash (省钱开发)
# staging: deepseek-v4-flash # staging: deepseek-flash
# production: deepseek-v4-pro (质量优先) # production: deepseek-flash (Pro 可单独评估后配置)
AI_DEFAULT_MODEL=deepseek-v4-flash AI_DEFAULT_MODEL=deepseek-flash
AI_REQUEST_TIMEOUT_SEC=60 AI_REQUEST_TIMEOUT_SEC=60
...@@ -107,7 +107,8 @@ GEMINI_API_KEY= ...@@ -107,7 +107,8 @@ GEMINI_API_KEY=
# GEMINI_LIVE_MODEL=gemini-3.1-flash-live-preview # GEMINI_LIVE_MODEL=gemini-3.1-flash-live-preview
# 价格表(¥/M tokens)— 调价时改这里重启;不配走默认 # 价格表(¥/M tokens)— 调价时改这里重启;不配走默认
# AI_PRICE_TABLE_JSON={"deepseek-v4-pro":{"inHit":0.5,"inMiss":3.6,"out":25},"deepseek-v4-flash":{"inHit":0.07,"inMiss":0.5,"out":2}} # Flash 默认按官方人民币高峰价保守估算,空闲时段实际为一半;自定义表会整体替换默认表。
# AI_PRICE_TABLE_JSON={"deepseek-flash":{"inHit":0.04,"inMiss":2,"out":8}}
# ─── CORS / iframe ─────────────────────────────────────────────────── # ─── CORS / iframe ───────────────────────────────────────────────────
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
* pnpm ai:gen-script -- --pid=F1001 # 用 patient externalId(自动取该 patient active plan) * pnpm ai:gen-script -- --pid=F1001 # 用 patient externalId(自动取该 patient active plan)
* pnpm ai:gen-script -- --pid=F1001 --bust-cache # 跳过缓存重新调 * pnpm ai:gen-script -- --pid=F1001 --bust-cache # 跳过缓存重新调
* pnpm ai:gen-script -- --pid=F1001 --dry-run # 不写 PlanScript 表 * pnpm ai:gen-script -- --pid=F1001 --dry-run # 不写 PlanScript 表
* pnpm ai:gen-script -- --pid=F1001 --model=deepseek-v4-flash # 切模型 * pnpm ai:gen-script -- --pid=F1001 --model=deepseek-flash # 切模型
* *
* 输入完全确定(不重新 pull 患者数据),仅用于验证 prompt + model 改动效果。 * 输入完全确定(不重新 pull 患者数据),仅用于验证 prompt + model 改动效果。
*/ */
...@@ -61,7 +61,7 @@ Usage: ...@@ -61,7 +61,7 @@ Usage:
pnpm ai:gen-script -- --pid=<patient-external-id> pnpm ai:gen-script -- --pid=<patient-external-id>
pnpm ai:gen-script -- --pid=F1001 --bust-cache # 跳过缓存,真实调 LLM pnpm ai:gen-script -- --pid=F1001 --bust-cache # 跳过缓存,真实调 LLM
pnpm ai:gen-script -- --pid=F1001 --dry-run # 不写 PlanScript pnpm ai:gen-script -- --pid=F1001 --dry-run # 不写 PlanScript
pnpm ai:gen-script -- --pid=F1001 --model=deepseek-v4-flash pnpm ai:gen-script -- --pid=F1001 --model=deepseek-flash
Options: Options:
--plan-id=<uuid> 直接指定 plan id --plan-id=<uuid> 直接指定 plan id
...@@ -70,7 +70,7 @@ Options: ...@@ -70,7 +70,7 @@ Options:
--tenant=<str> 默认 t_demo --tenant=<str> 默认 t_demo
--bust-cache 强制重跑(不走 prompt cache) --bust-cache 强制重跑(不走 prompt cache)
--dry-run 只生成不写库 --dry-run 只生成不写库
--model=<id> 覆盖默认模型(deepseek-v4-pro / deepseek-v4-flash) --model=<id> 覆盖默认模型(deepseek-v4-pro / deepseek-flash)
`); `);
} }
......
/** 官方保留的旧 Flash 别名;统一请求名和调用记录,不改历史数据。 */
export function canonicalModelId(modelId: string): string {
return modelId === 'deepseek-v4-flash' || modelId === 'deepseek-v4-flash-vision-exp'
? 'deepseek-flash'
: modelId;
}
import { canonicalModelId } from './ai-models';
export interface AppConfig { export interface AppConfig {
nodeEnv: 'development' | 'test' | 'production'; nodeEnv: 'development' | 'test' | 'production';
port: number; port: number;
...@@ -15,7 +17,7 @@ export interface AppConfig { ...@@ -15,7 +17,7 @@ export interface AppConfig {
deepseekApiKey: string; deepseekApiKey: string;
/// DeepSeek base URL(默认官方;私有 / 镜像 endpoint 时覆盖) /// DeepSeek base URL(默认官方;私有 / 镜像 endpoint 时覆盖)
deepseekBaseUrl: string; deepseekBaseUrl: string;
/// 默认主力模型(deepseek-v4-pro / deepseek-v4-flash) /// 默认主力模型(deepseek-v4-pro / deepseek-flash)
defaultModel: string; defaultModel: string;
/// Gemini API key(Google AI Studio;海外需可达 generativelanguage.googleapis.com) /// Gemini API key(Google AI Studio;海外需可达 generativelanguage.googleapis.com)
geminiApiKey: string; geminiApiKey: string;
...@@ -75,7 +77,7 @@ export function loadConfig(): AppConfig { ...@@ -75,7 +77,7 @@ export function loadConfig(): AppConfig {
ai: { ai: {
deepseekApiKey: process.env.DEEPSEEK_API_KEY ?? '', deepseekApiKey: process.env.DEEPSEEK_API_KEY ?? '',
deepseekBaseUrl: process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com', deepseekBaseUrl: process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com',
defaultModel: process.env.AI_DEFAULT_MODEL ?? 'deepseek-v4-flash', defaultModel: canonicalModelId(process.env.AI_DEFAULT_MODEL ?? 'deepseek-flash'),
geminiApiKey: process.env.GEMINI_API_KEY ?? '', geminiApiKey: process.env.GEMINI_API_KEY ?? '',
geminiBaseUrl: process.env.GEMINI_BASE_URL ?? '', geminiBaseUrl: process.env.GEMINI_BASE_URL ?? '',
geminiDefaultModel: process.env.GEMINI_DEFAULT_MODEL ?? 'gemini-3.5-flash', geminiDefaultModel: process.env.GEMINI_DEFAULT_MODEL ?? 'gemini-3.5-flash',
...@@ -108,8 +110,8 @@ export function loadConfig(): AppConfig { ...@@ -108,8 +110,8 @@ export function loadConfig(): AppConfig {
/** /**
* AI 价格表 — 从 env JSON 读,失败兜底默认值。 * AI 价格表 — 从 env JSON 读,失败兜底默认值。
* *
* 单位:¥/M tokens(已按汇率换算,DeepSeek 公开价 USD * 7.2)。 * 单位:¥/M tokens。DeepSeek Flash 使用官方人民币高峰价作保守估算,非实际账单。
* inHit: 输入命中 prompt cache 的部分(便宜 ~7x) * inHit: 输入命中 prompt cache 的部分
* inMiss: 输入未命中(全价) * inMiss: 输入未命中(全价)
* out: 输出(含 reasoning,DeepSeek 统一计费) * out: 输出(含 reasoning,DeepSeek 统一计费)
* *
...@@ -120,14 +122,16 @@ export function loadConfig(): AppConfig { ...@@ -120,14 +122,16 @@ export function loadConfig(): AppConfig {
* 4. 新调用按新价计 * 4. 新调用按新价计
*/ */
function parsePriceTable(raw: string | undefined): Record<string, { inHit: number; inMiss: number; out: number }> { function parsePriceTable(raw: string | undefined): Record<string, { inHit: number; inMiss: number; out: number }> {
// ⚠️ 价格核对于 2026-06(官方页):¥/M tokens。美元价按 ×7.2 折 RMB。 // 除下方单独更新的 Flash 外,其他价格沿用 2026-06 配置:¥/M tokens,美元价按 ×7.2 折 RMB。
// 用各家**标准价**(不含限时促销,促销到期不会少报);要按实付/促销算 → 用 AI_PRICE_TABLE_JSON 覆盖。 // 用各家**标准价**(不含限时促销,促销到期不会少报);要按实付/促销算 → 用 AI_PRICE_TABLE_JSON 覆盖。
// DeepSeek:api-docs.deepseek.com/quick_start/pricing(V4-pro $0.003625/0.435/0.87、V4-flash $0.0028/0.14/0.28) // Flash 于 2026-10-23 单独核对官方人民币价:高峰 0.04/2/8,空闲为一半。
// https://api-docs.deepseek.com/zh-cn/quick_start/pricing/
// 仍用固定单价保守估算,不模拟节假日峰谷账单;其他模型价格保持原配置。
// Gemini 3.5 Flash:$0.15/1.5/9.0(cache/in/out) // Gemini 3.5 Flash:$0.15/1.5/9.0(cache/in/out)
// Qwen3.7-Max(百炼,RMB 直接):缓存输入 1.2 / 输入 12 / 输出 36;现有限时 5 折 = 6/18(促销价走 env) // Qwen3.7-Max(百炼,RMB 直接):缓存输入 1.2 / 输入 12 / 输出 36;现有限时 5 折 = 6/18(促销价走 env)
const DEFAULT = { const DEFAULT = {
'deepseek-v4-pro': { inHit: 0.03, inMiss: 3.13, out: 6.26 }, 'deepseek-v4-pro': { inHit: 0.03, inMiss: 3.13, out: 6.26 },
'deepseek-v4-flash': { inHit: 0.02, inMiss: 1.01, out: 2.02 }, 'deepseek-flash': { inHit: 0.04, inMiss: 2, out: 8 },
'gemini-3.5-flash': { inHit: 1.08, inMiss: 10.8, out: 64.8 }, 'gemini-3.5-flash': { inHit: 1.08, inMiss: 10.8, out: 64.8 },
'gemini-2.5-flash': { inHit: 1.08, inMiss: 10.8, out: 64.8 }, 'gemini-2.5-flash': { inHit: 1.08, inMiss: 10.8, out: 64.8 },
// Qwen3.7-Max 是旗舰最贵档(不是便宜模型):标准价 ¥/M 直接填 // Qwen3.7-Max 是旗舰最贵档(不是便宜模型):标准价 ¥/M 直接填
......
...@@ -56,7 +56,7 @@ export class DeepPlanCall implements AiCall<ScriptContext, DeepPlanZ> { ...@@ -56,7 +56,7 @@ export class DeepPlanCall implements AiCall<ScriptContext, DeepPlanZ> {
readonly kind = 'script' as const; readonly kind = 'script' as const;
readonly callKey = 'draft_plan_script_plan'; readonly callKey = 'draft_plan_script_plan';
readonly promptVersion = 'draft_plan_script@2026-06-16-deep-plan-v9'; // v9: 去掉钦定段式(给自由度),只留从果推因/层层递进原则;v8: 从果推因+后果不吓唬+3-6段 readonly promptVersion = 'draft_plan_script@2026-06-16-deep-plan-v9'; // v9: 去掉钦定段式(给自由度),只留从果推因/层层递进原则;v8: 从果推因+后果不吓唬+3-6段
readonly defaultModelId = 'deepseek-v4-flash'; readonly defaultModelId = 'deepseek-flash';
readonly outputSchema = DeepPlanSchema; readonly outputSchema = DeepPlanSchema;
buildPrompt(ctx: ScriptContext) { buildPrompt(ctx: ScriptContext) {
return { system: PLAN_SYSTEM, prompt: buildPlanPrompt(ctx) }; return { system: PLAN_SYSTEM, prompt: buildPlanPrompt(ctx) };
...@@ -68,7 +68,7 @@ export class DeepWriteCall implements AiCall<DeepWriteInput, DeepWriteZ> { ...@@ -68,7 +68,7 @@ export class DeepWriteCall implements AiCall<DeepWriteInput, DeepWriteZ> {
readonly kind = 'script' as const; readonly kind = 'script' as const;
readonly callKey = 'draft_plan_script_write'; readonly callKey = 'draft_plan_script_write';
readonly promptVersion = 'draft_plan_script@2026-07-29-deep-write-v19'; // v19: 自报家门改「我是{诊断医生}医生的助理X」(业务口径:非客服身份)。v18: 低龄约束(<18 不主推种植)+ 替牙期(≤12)说「早期矫治」。v17: 高龄硬约束(≥75 缺牙禁推种植/手术)。v16: 自报家门去人名 → 占位【回访客服】(渲染期按登录人回填,修串名 bug)。v15: 去掉钦定段式(给自由度);v14: 禁内部代码/术语(K08等)说大白话 readonly promptVersion = 'draft_plan_script@2026-07-29-deep-write-v19'; // v19: 自报家门改「我是{诊断医生}医生的助理X」(业务口径:非客服身份)。v18: 低龄约束(<18 不主推种植)+ 替牙期(≤12)说「早期矫治」。v17: 高龄硬约束(≥75 缺牙禁推种植/手术)。v16: 自报家门去人名 → 占位【回访客服】(渲染期按登录人回填,修串名 bug)。v15: 去掉钦定段式(给自由度);v14: 禁内部代码/术语(K08等)说大白话
readonly defaultModelId = 'deepseek-v4-flash'; readonly defaultModelId = 'deepseek-flash';
readonly outputSchema = DeepWriteSchema; readonly outputSchema = DeepWriteSchema;
constructor(private readonly skillRegistry: DraftPlanScriptSkillRegistry) {} constructor(private readonly skillRegistry: DraftPlanScriptSkillRegistry) {}
buildPrompt(input: DeepWriteInput) { buildPrompt(input: DeepWriteInput) {
...@@ -86,7 +86,7 @@ export class DeepVerifyCall implements AiCall<DeepVerifyInput, DeepVerifyZ> { ...@@ -86,7 +86,7 @@ export class DeepVerifyCall implements AiCall<DeepVerifyInput, DeepVerifyZ> {
readonly kind = 'judge' as const; readonly kind = 'judge' as const;
readonly callKey = 'draft_plan_script_verify'; readonly callKey = 'draft_plan_script_verify';
readonly promptVersion = 'draft_plan_script@2026-06-16-deep-verify-v10'; // v10: 递进检查去钦定段式(给自由度);v9: 加④患者听得懂(无代码/术语/含糊) readonly promptVersion = 'draft_plan_script@2026-06-16-deep-verify-v10'; // v10: 递进检查去钦定段式(给自由度);v9: 加④患者听得懂(无代码/术语/含糊)
readonly defaultModelId = 'deepseek-v4-flash'; readonly defaultModelId = 'deepseek-flash';
readonly outputSchema = DeepVerifySchema; readonly outputSchema = DeepVerifySchema;
buildPrompt(input: DeepVerifyInput) { buildPrompt(input: DeepVerifyInput) {
return { system: VERIFY_SYSTEM, prompt: buildVerifyPrompt(input) }; return { system: VERIFY_SYSTEM, prompt: buildVerifyPrompt(input) };
......
...@@ -77,7 +77,7 @@ export class DraftPlanScriptCall ...@@ -77,7 +77,7 @@ export class DraftPlanScriptCall
readonly kind = 'script' as const; readonly kind = 'script' as const;
readonly callKey = 'draft_plan_script'; readonly callKey = 'draft_plan_script';
readonly promptVersion = DRAFT_PLAN_SCRIPT_PROMPT_VERSION; readonly promptVersion = DRAFT_PLAN_SCRIPT_PROMPT_VERSION;
readonly defaultModelId = 'deepseek-v4-flash'; readonly defaultModelId = 'deepseek-flash';
readonly outputSchema = DraftPlanScriptSchema; readonly outputSchema = DraftPlanScriptSchema;
readonly safetyRules = safetyRules; readonly safetyRules = safetyRules;
......
...@@ -30,7 +30,7 @@ export class StandardScriptCall implements AiCall<DraftPlanScriptInput, DeepDraf ...@@ -30,7 +30,7 @@ export class StandardScriptCall implements AiCall<DraftPlanScriptInput, DeepDraf
readonly kind = 'script' as const; readonly kind = 'script' as const;
readonly callKey = 'draft_plan_script'; readonly callKey = 'draft_plan_script';
readonly promptVersion = STANDARD_PROMPT_VERSION; readonly promptVersion = STANDARD_PROMPT_VERSION;
readonly defaultModelId = 'deepseek-v4-flash'; readonly defaultModelId = 'deepseek-flash';
readonly outputSchema = StandardScriptSchema; readonly outputSchema = StandardScriptSchema;
// 显式标 SafetyRule<DeepDraft>,让 runner.run 的 TOutput 推成 DeepDraft(否则会被结构型窄化丢 tone) // 显式标 SafetyRule<DeepDraft>,让 runner.run 的 TOutput 推成 DeepDraft(否则会被结构型窄化丢 tone)
readonly safetyRules: ReadonlyArray<SafetyRule<DeepDraft>> = SCRIPT_SAFETY_RULES_SECTIONS; readonly safetyRules: ReadonlyArray<SafetyRule<DeepDraft>> = SCRIPT_SAFETY_RULES_SECTIONS;
......
...@@ -52,7 +52,7 @@ export class WecomWriteCall implements AiCall<WecomWriteInput, WecomWriteZ> { ...@@ -52,7 +52,7 @@ export class WecomWriteCall implements AiCall<WecomWriteInput, WecomWriteZ> {
readonly callKey = 'draft_wecom_script_write'; readonly callKey = 'draft_wecom_script_write';
// ⚠️ 换 promptVersion 会让缓存整体失效(这是想要的):流程和形态都变了,旧缓存不该再命中 // ⚠️ 换 promptVersion 会让缓存整体失效(这是想要的):流程和形态都变了,旧缓存不该再命中
readonly promptVersion = 'draft_wecom_script@2026-08-05-single-v2'; readonly promptVersion = 'draft_wecom_script@2026-08-05-single-v2';
readonly defaultModelId = 'deepseek-v4-flash'; readonly defaultModelId = 'deepseek-flash';
readonly outputSchema = WecomWriteSchema; readonly outputSchema = WecomWriteSchema;
constructor(private readonly skillRegistry: DraftPlanScriptSkillRegistry) {} constructor(private readonly skillRegistry: DraftPlanScriptSkillRegistry) {}
buildPrompt(input: WecomWriteInput) { buildPrompt(input: WecomWriteInput) {
......
...@@ -5,6 +5,7 @@ import { createGoogleGenerativeAI, type GoogleGenerativeAIProvider } from '@ai-s ...@@ -5,6 +5,7 @@ import { createGoogleGenerativeAI, type GoogleGenerativeAIProvider } from '@ai-s
import { createOpenAICompatible, type OpenAICompatibleProvider } from '@ai-sdk/openai-compatible'; import { createOpenAICompatible, type OpenAICompatibleProvider } from '@ai-sdk/openai-compatible';
import type { LanguageModel } from 'ai'; import type { LanguageModel } from 'ai';
import type { AppConfig } from '../../../config/configuration'; import type { AppConfig } from '../../../config/configuration';
import { canonicalModelId } from '../../../config/ai-models';
/** /**
* AiProviderService — 模型工厂。 * AiProviderService — 模型工厂。
...@@ -31,7 +32,7 @@ export class AiProviderService { ...@@ -31,7 +32,7 @@ export class AiProviderService {
constructor(private readonly config: ConfigService<AppConfig, true>) { constructor(private readonly config: ConfigService<AppConfig, true>) {
const ai = this.config.get('ai', { infer: true }); const ai = this.config.get('ai', { infer: true });
this.defaultModel = ai.defaultModel; this.defaultModel = canonicalModelId(ai.defaultModel);
this.geminiDefaultModel = ai.geminiDefaultModel; this.geminiDefaultModel = ai.geminiDefaultModel;
this.qwenDefaultModel = ai.qwenDefaultModel; this.qwenDefaultModel = ai.qwenDefaultModel;
...@@ -105,14 +106,14 @@ export class AiProviderService { ...@@ -105,14 +106,14 @@ export class AiProviderService {
/** /**
* 解析 modelId → vendor 实例 + 规范化具体 modelId(落账 modelName / 计费 priceTable 用规范化后的)。 * 解析 modelId → vendor 实例 + 规范化具体 modelId(落账 modelName / 计费 priceTable 用规范化后的)。
* 命名约定: * 命名约定:
* "deepseek-v4-pro" / "deepseek-v4-flash" / "deepseek"(裸键)→ deepseek * "deepseek-v4-pro" / "deepseek-flash" / "deepseek"(裸键)→ deepseek
* "gemini-2.5-flash" / "gemini"(裸键) → gemini * "gemini-2.5-flash" / "gemini"(裸键) → gemini
* "qwen-max"(将来) → qwen * "qwen-max"(将来) → qwen
* 裸键(无 "-" 版本)→ 解析到 config 配的默认型号;带版本 → 原样直传 SDK。 * 裸键(无 "-" 版本)→ 解析到 config 配的默认型号;具体型号 → 直传 SDK(旧 Flash 别名归一为 deepseek-flash)。
*/ */
resolve(modelId: string): { model: LanguageModel; provider: string; modelId: string } { resolve(modelId: string): { model: LanguageModel; provider: string; modelId: string } {
if (modelId === 'deepseek' || modelId.startsWith('deepseek-')) { if (modelId === 'deepseek' || modelId.startsWith('deepseek-')) {
const canonical = modelId === 'deepseek' ? this.defaultModel : modelId; const canonical = canonicalModelId(modelId === 'deepseek' ? this.defaultModel : modelId);
return { model: this.deepseek(canonical), provider: 'deepseek', modelId: canonical }; return { model: this.deepseek(canonical), provider: 'deepseek', modelId: canonical };
} }
if (modelId === 'gemini' || modelId.startsWith('gemini-')) { if (modelId === 'gemini' || modelId.startsWith('gemini-')) {
......
import { canonicalModelId } from '../../../config/ai-models';
/** /**
* 成本估算 —— 纯函数,`AiCallRunner` 与助手共用**同一份**口径。 * 成本估算 —— 纯函数,`AiCallRunner` 与助手共用**同一份**口径。
* *
...@@ -30,7 +32,11 @@ export function estimateCostYuan( ...@@ -30,7 +32,11 @@ export function estimateCostYuan(
completionTokens: number, completionTokens: number,
cachedInputTokens = 0, cachedInputTokens = 0,
): CostResult { ): CostResult {
const p = priceTable[modelId]; // 新旧 Flash 名使用同一计价;兼容部署时仍按旧名称配置的自定义价目表。
const canonical = canonicalModelId(modelId);
const p = priceTable[canonical] ?? priceTable[modelId] ?? (canonical === 'deepseek-flash'
? priceTable['deepseek-v4-flash'] ?? priceTable['deepseek-v4-flash-vision-exp']
: undefined);
const price = p ?? priceTable['deepseek-v4-pro'] ?? FALLBACK_PRICE; const price = p ?? priceTable['deepseek-v4-pro'] ?? FALLBACK_PRICE;
// 防御:cached > prompt 不该发生,clamp // 防御:cached > prompt 不该发生,clamp
const hit = Math.min(Math.max(0, cachedInputTokens), Math.max(0, promptTokens)); const hit = Math.min(Math.max(0, cachedInputTokens), Math.max(0, promptTokens));
......
...@@ -270,7 +270,7 @@ export class PlanScriptOrchestrator { ...@@ -270,7 +270,7 @@ export class PlanScriptOrchestrator {
yield { yield {
type: 'start', type: 'start',
invocationId: '', invocationId: '',
modelId: options.modelIdOverride ?? 'deepseek-v4-flash', modelId: options.modelIdOverride ?? 'deepseek-flash',
promptVersion: 'draft_plan_script@deep-pipeline', promptVersion: 'draft_plan_script@deep-pipeline',
}; };
let r: DeepScriptResult; let r: DeepScriptResult;
......
import { loadConfig } from '../src/config/configuration';
import { AiProviderService } from '../src/modules/ai/core/ai-provider.service';
import { estimateCostYuan } from '../src/modules/ai/core/cost';
import { ConfigService } from '@nestjs/config';
import type { AppConfig } from '../src/config/configuration';
jest.mock('@ai-sdk/deepseek', () => ({ createDeepSeek: () => (id: string) => ({ modelId: id }) }));
jest.mock('@ai-sdk/google', () => ({ createGoogleGenerativeAI: () => (id: string) => ({ modelId: id }) }));
jest.mock('@ai-sdk/openai-compatible', () => ({ createOpenAICompatible: () => (id: string) => ({ modelId: id }) }));
describe('DeepSeek Flash 型号与计价迁移', () => {
const originalEnv = process.env;
beforeEach(() => {
process.env = {
DATABASE_URL: 'postgresql://unused', REDIS_URL: 'redis://unused',
JWT_SECRET: 'test-only', JWT_REFRESH_SECRET: 'test-only',
DEEPSEEK_API_KEY: 'test-only', GEMINI_API_KEY: 'test-only', QWEN_API_KEY: 'test-only',
};
});
afterEach(() => { process.env = originalEnv; });
function provider() {
const config = loadConfig();
return new AiProviderService(new ConfigService<AppConfig, true>(config));
}
it('默认配置、SDK 请求和落账型号一致', () => {
expect(loadConfig().ai.defaultModel).toBe('deepseek-flash');
const resolved = provider().resolve('deepseek');
expect(resolved.modelId).toBe('deepseek-flash');
expect(resolved.model).toMatchObject({ modelId: 'deepseek-flash' });
expect(resolved.provider).toBe('deepseek');
});
it.each(['deepseek-v4-flash', 'deepseek-v4-flash-vision-exp'])('兼容环境变量和显式旧别名 %s', alias => {
process.env.AI_DEFAULT_MODEL = alias;
expect(loadConfig().ai.defaultModel).toBe('deepseek-flash');
expect(provider().getDefaultModelId()).toBe('deepseek-flash');
expect(provider().resolve(alias).model).toMatchObject({ modelId: 'deepseek-flash' });
});
it('保留显式 Pro 配置与其他供应商路由', () => {
process.env.AI_DEFAULT_MODEL = 'deepseek-v4-pro';
expect(provider().resolve('deepseek').modelId).toBe('deepseek-v4-pro');
expect(provider().resolve('qwen').provider).toBe('qwen');
expect(provider().resolve('gemini').provider).toBe('gemini');
});
it.each(['deepseek-flash', 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp'])('%s 按 Flash 价估算,不误落 Pro 兜底', model => {
const table = loadConfig().ai.priceTable;
expect(estimateCostYuan(table, model, 1_000_000, 1_000_000, 500_000))
.toEqual({ yuan: 9.02, priceMissing: false });
});
it('沿用旧名称的自定义价格,不悄悄替换用户配置', () => {
process.env.AI_PRICE_TABLE_JSON = JSON.stringify({ 'deepseek-v4-flash': { inHit: 1, inMiss: 2, out: 3 } });
expect(estimateCostYuan(loadConfig().ai.priceTable, 'deepseek-flash', 1_000_000, 1_000_000))
.toEqual({ yuan: 5, priceMissing: false });
});
it('自定义价格同时有新旧名称时,以新名称为准', () => {
const table = {
'deepseek-flash': { inHit: 1, inMiss: 2, out: 3 },
'deepseek-v4-flash': { inHit: 7, inMiss: 8, out: 9 },
};
expect(estimateCostYuan(table, 'deepseek-v4-flash', 1_000_000, 1_000_000))
.toEqual({ yuan: 5, priceMissing: false });
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment