新增: F-01阶段5模型路由调用点接入

This commit is contained in:
2026-06-17 00:33:44 +08:00
parent 5b7f342e14
commit 2ddfea67c7
10 changed files with 220 additions and 24 deletions

1
Cargo.lock generated
View File

@@ -820,6 +820,7 @@ dependencies = [
"anyhow",
"async-trait",
"chrono",
"df-ai",
"df-ai-core",
"df-core",
"regex",

View File

@@ -10,7 +10,10 @@
use std::cmp::Reverse;
use df_ai_core::model::{Capability, CostTier, IntelligenceTier, Modality, ModelConfig};
// 阶段5: 调用点经 `df_ai::router::{Modality, Capability, CostTier, IntelligenceTier}`
// 直接 import 维度枚举构造 TaskRequirements(对齐任务规格 import 风格),re-export 避免调用点
// 各自从 df_ai_core::model 取(跨 crate 路径冗长)。select/select_model_id 仅借用枚举,无重定义。
pub use df_ai_core::model::{Capability, CostTier, IntelligenceTier, Modality, ModelConfig};
/// 任务对模型的需求(5 维度,对齐设计 §6.1)。
///
@@ -63,6 +66,20 @@ impl ModelRouter {
}
}
/// 阶段5 调用点 helper — 路由选模型并直接返回 model_id(纯函数)。
///
/// 给定 TaskRequirements + 候选池,返回最优模型的 `model_id`。
/// 调用点用法:`provider.model_configs`(Vec<ModelConfig>)→ `select_model_id(&req, &pool)`
/// → `Option<String>`;None 时兜底 `provider.default_model`(行为不变,平滑过渡)。
///
/// 行为不变保证:
/// - 池空(用户未通过 Settings 拉取模型)→ 返回 None → 调用点兜底 default_model
/// - 池非空但无候选满足需求 → 返回 None → 兜底 default_model
/// - 池非空命中 → 返回 model_id(F-01 路由目标,拉取即启用路由)
pub fn select_model_id(req: &TaskRequirements, pool: &[ModelConfig]) -> Option<String> {
ModelRouter::select(req, pool).map(|m| m.model_id.clone())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -94,6 +111,22 @@ mod tests {
}
}
// ── 阶段5 select_model_id helper ──
#[test]
fn select_model_id_empty_pool_returns_none() {
// 池空(用户未拉取模型)→ None,调用点兜底 default_model
let pool: Vec<ModelConfig> = vec![];
assert!(select_model_id(&req(), &pool).is_none());
}
#[test]
fn select_model_id_hit_returns_model_id() {
// 池非空命中 → 返回 model_id 字符串(非引用)
let pool = vec![model("glm-4-flash")];
assert_eq!(select_model_id(&req(), &pool).as_deref(), Some("glm-4-flash"));
}
// ── 步骤 1:enabled 过滤 ──
#[test]

View File

@@ -6,6 +6,10 @@ edition = "2021"
[dependencies]
df-core = { path = "../df-core" }
df-ai-core = { path = "../df-ai-core" }
# F-01 阶段5: df-ideas 对抗评估接入 df_ai::router::select_model_id(纯函数路由)。
# 注:df-ai 引入 reqwest/futures/eventsource-stream 重依赖,但 router 模块仅依赖
# df-ai-core::model,实际编译期 df-ideas 仅引用 router 符号(零 HTTP 代码路径)。
df-ai = { path = "../df-ai" }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }

View File

@@ -16,6 +16,7 @@ use std::sync::Arc;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use df_ai_core::model::ModelConfig;
use df_ai_core::provider::LlmProvider;
use df_core::types::{IdeaId, Priority};
use crate::capture::Idea;
@@ -95,17 +96,26 @@ pub struct AdversarialEngine {
/// 可选 LLM provider。Some → 优先 LLM 评估失败降级启发式None → 纯启发式。
/// 构造注入(与 IdeaPromoter::new(policy) 同一模式),批量评估复用同一 provider。
provider: Option<Arc<dyn LlmProvider>>,
/// F-01 阶段5: 候选模型池。非空时 evaluate_with_llm 经 select_model_id 路由选模型;
/// 空(None provider 或未注入池)→ model 留空由 provider impl 回填自身 default_model
/// (与接入前行为一致,平稳过渡)。
model_pool: Vec<ModelConfig>,
}
impl AdversarialEngine {
/// 注入 LLM provider 构造provider Some 时走 LLM调用失败自动降级启发式
pub fn new(provider: Arc<dyn LlmProvider>) -> Self {
Self { provider: Some(provider) }
Self { provider: Some(provider), model_pool: Vec::new() }
}
/// F-01 阶段5: 注入 provider + 候选模型池构造。池非空时 evaluate_with_llm 走路由。
pub fn with_pool(provider: Arc<dyn LlmProvider>, model_pool: Vec<ModelConfig>) -> Self {
Self { provider: Some(provider), model_pool }
}
/// 纯启发式构造(无 LLM 配置时的默认模式)
pub fn heuristic() -> Self {
Self { provider: None }
Self { provider: None, model_pool: Vec::new() }
}
/// 执行完整的对抗评估(内部按 provider 有无调度 LLM / 启发式,失败降级)
@@ -142,10 +152,21 @@ impl AdversarialEngine {
/// 0.4 在「稳定可复现」与「论点多样性」间取得平衡。
async fn evaluate_with_llm(&self, idea: &Idea, provider: &Arc<dyn LlmProvider>) -> Result<AdversarialEval> {
let prompt = build_adversarial_prompt(idea);
// F-01 阶段5: 智能路由 — 对抗评估 TaskRequirements(Standard,无工具)。
// 池非空 → select_model_id 选最优 model_id;池空/无匹配 → 留空由 provider impl
// 回填自身 default_model(与接入前行为一致,平稳过渡)。
let eval_req = df_ai::router::TaskRequirements {
modalities: vec![df_ai_core::model::Modality::Text],
needs_tool_use: false,
min_intelligence: df_ai_core::model::IntelligenceTier::Standard,
max_cost: None,
estimated_context: 0,
};
let model = df_ai::router::select_model_id(&eval_req, &self.model_pool).unwrap_or_default();
let request = df_ai_core::provider::CompletionRequest {
// provider 自带 default_modelmodel 留空让 provider impl 回填自身默认模型
// 路由命中 → 用 model_id;否则留空让 provider impl 回填自身 default_model
// (OpenAICompatProvider::convert_request 在 req.model.is_empty() 时回退 default_model)
model: String::new(),
model,
messages: vec![
df_ai_core::provider::ChatMessage::system(SYSTEM_PROMPT),
df_ai_core::provider::ChatMessage::user(prompt),

View File

@@ -10,7 +10,11 @@ use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use df_ai::df_ai_core::model::{IntelligenceTier, Modality, ModelConfig};
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider};
// F-01 阶段5: AiNode 路由 — 节点 config.model_id 优先;否则按 TaskRequirements 路由
// (默认 Standard + needs_tool_use=true)。池空/无匹配兜底 record.default_model。
use df_ai::router::{select_model_id, TaskRequirements};
use df_storage::crud::{AiProviderRepo, TaskRepo};
use df_storage::db::Database;
use df_storage::models::AiProviderRecord;
@@ -45,6 +49,9 @@ struct ResolvedProvider {
api_key: String,
/// model 为空时的占位record.default_model 或 "gpt-4o-mini"),避免 provider 构造 panic
default_model: String,
/// F-01 阶段5: 候选模型池(来自 record.model_configs)。parse_params 路由用:
/// config.model 留空时经 select_model_id 选最优;池空兜底 default_model。
model_pool: Vec<ModelConfig>,
}
/// 经 ai_providers 表 + df_storage::secret 解析 provider 构造要素FR-S1 注入链核心)。
@@ -117,6 +124,8 @@ async fn resolve_provider(
base_url,
api_key,
default_model,
// 老明文路径无 record,候选池空(无路由能力,兜底 default_model)。
model_pool: Vec::new(),
});
}
@@ -154,6 +163,7 @@ fn resolve_from_record(
base_url: record.base_url.clone(),
api_key,
default_model,
model_pool: record.model_configs.clone(),
})
}
@@ -179,11 +189,30 @@ fn parse_params(
.ok_or_else(|| anyhow::anyhow!("AiNode 缺少必填参数: promptconfig 或上游输入均无)"))?;
// ── 可选参数 ──
let model = config
// model 解析优先级(F-01 阶段5):config.model 显式指定 > 路由选优(provider.model_pool 非空时)
// > 空(CompletionRequest.model 留空由 provider impl 回填 default_model,行为不变)。
// 注:provider.model_pool 在 provider move 进 AiNodeParams 前先借引用路由,选中的 model_id
// 填入 CompletionRequest.model;provider.default_model 仍是 build_provider 兜底用。
let config_model = config
.get("model")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let model = if !config_model.is_empty() {
config_model
} else {
// F-01 阶段5: AiNode 默认路由 — Standard + needs_tool_use=true(工作流无人值守 AI 步骤
// 常含工具调用,如检索/生成;无需工具的节点应在 config 显式指定 model)。
// select_model_id None(池空/无匹配)→ 空串(由 provider impl 回填 default_model)。
let node_req = TaskRequirements {
modalities: vec![Modality::Text],
needs_tool_use: true,
min_intelligence: IntelligenceTier::Standard,
max_cost: None,
estimated_context: 0,
};
select_model_id(&node_req, &provider.model_pool).unwrap_or_default()
};
let temperature = config
.get("temperature")
.and_then(|v| v.as_f64())
@@ -600,6 +629,7 @@ mod tests {
base_url: "https://api.example.com".to_string(),
api_key: "sk-test".to_string(),
default_model: "gpt-4o-mini".to_string(),
model_pool: Vec::new(),
}
}

View File

@@ -12,6 +12,13 @@ use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider};
// CR-30-1: 复用 retry::backoff_delay(jitter 1s→2s→4s) + is_status_retryable(Fatal 分类)
// 实现流前失败重试退避对齐(决策 F-260616-07 a1),避免重写退避逻辑。
use df_ai::retry;
// F-01 阶段5: 智能路由 helper + TaskRequirements + 维度枚举。
// 调用点构造 TaskRequirements(主对话:needs_tool_use=true,min_intelligence=Standard,
// 当前仅 Text 模态;后续多模态接入时检测消息内 Part/Image 追加 Vision),
// 经 select_model_id 在 provider.model_configs 池中选最优;池空/无匹配兜底 default_model。
use df_ai::router::{
select_model_id, IntelligenceTier, Modality, TaskRequirements,
};
use df_storage::db::Database;
use df_storage::models::AiProviderRecord;
@@ -154,6 +161,19 @@ pub(crate) async fn run_agentic_loop(
key_len = key_len,
"[ai] 发起 LLM 请求"
);
// F-01 阶段5: 主对话路由 — TaskRequirements(needs_tool_use=true,min_intelligence=Standard)。
// 模态当前仅 Text(图像消息类型未实现,后续多模态接入时检测 Part/Image 追加 Vision)。
// select_model_id None(池空/无匹配)→ 兜底 default_model,行为与接入前一致。
let agentic_req = TaskRequirements {
modalities: vec![Modality::Text],
needs_tool_use: true,
min_intelligence: IntelligenceTier::Standard,
max_cost: None,
estimated_context: 0,
};
let resolved_model = select_model_id(&agentic_req, &provider_config.model_configs)
.unwrap_or_else(|| provider_config.default_model.clone());
let tool_defs = tools_arc.tool_definitions();
// 停止信号副本stream_llm 与每轮迭代共享读取,避免重复加锁
// notify 同取一份 Arc 引用B-260615-14stream_llm select! 监听 notified() 即时唤醒
@@ -265,7 +285,7 @@ pub(crate) async fn run_agentic_loop(
// 一致,重建与复用等价。收益:省去每次重试的 lock() + build_for_request(含
// all_messages_clone 全量 clone)。
let retry_request = CompletionRequest {
model: provider_config.default_model.clone(),
model: resolved_model.clone(),
messages: messages.clone(),
temperature: Some(0.7),
max_tokens: Some(8192),
@@ -387,16 +407,16 @@ pub(crate) async fn run_agentic_loop(
}
if !full_text.is_empty() {
let mut msg = ChatMessage::assistant(&full_text);
msg.model = Some(provider_config.default_model.clone());
msg.model = Some(resolved_model.clone());
session.messages.push(msg);
// 追加系统提示消息:响应因网络中断不完整(对齐决策 a1 系统提示机制)
let mut notice = ChatMessage::system("⚠ 响应因网络中断不完整,以上为已接收的部分内容。可重新发送以获取完整回复。");
notice.model = Some(provider_config.default_model.clone());
notice.model = Some(resolved_model.clone());
session.messages.push(notice);
}
}
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&provider_config.default_model)).await;
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model)).await;
// 标题生成后台化(失败有 extract_title 兜底)
spawn_ensure_title(&provider_config, &db, &conv_id, &app_handle, &session_arc, &llm_concurrency);
guard.reset().await;
@@ -442,11 +462,11 @@ pub(crate) async fn run_agentic_loop(
})
.collect();
let mut msg = ChatMessage::assistant_with_tools(&full_text, ai_tool_calls);
msg.model = Some(provider_config.default_model.clone());
msg.model = Some(resolved_model.clone());
session.messages.push(msg);
} else if !full_text.is_empty() {
let mut msg = ChatMessage::assistant(&full_text);
msg.model = Some(provider_config.default_model.clone());
msg.model = Some(resolved_model.clone());
session.messages.push(msg);
}
}
@@ -458,7 +478,7 @@ pub(crate) async fn run_agentic_loop(
completion_tokens: tokens.completion(),
total_tokens: tokens.total(),
};
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&provider_config.default_model)).await;
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model)).await;
// 标题生成后台化:不阻塞 Completed emit失败有 extract_title 兜底)
spawn_ensure_title(&provider_config, &db, &conv_id, &app_handle, &session_arc, &llm_concurrency);
guard.reset().await;
@@ -483,7 +503,7 @@ pub(crate) async fn run_agentic_loop(
completion_tokens: tokens.completion(),
total_tokens: tokens.total(),
};
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&provider_config.default_model)).await;
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model)).await;
// B-260615-26: 审批等待 return 前 disarm guard——保持 generating=true 留 try_continue 续生成,
// 同时 Drop 因 done=true 跳过复位 spawn(避免误复位审批态 generating 致 ai_approve→try_continue 不续)
guard.disarm();
@@ -512,7 +532,7 @@ pub(crate) async fn run_agentic_loop(
completion_tokens: tokens.completion(),
total_tokens: tokens.total(),
};
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&provider_config.default_model)).await;
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model)).await;
// 暂停态保持 generating=true(防其他 send 抢占,仿审批),disarm guard 跳过 Drop 兜底复位
guard.disarm();
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiMaxRoundsReached {
@@ -540,8 +560,9 @@ pub(crate) async fn run_agentic_loop(
let knowledge_config = knowledge_config.clone();
let app_handle = app_handle.clone();
let llm_concurrency = llm_concurrency.clone();
let resolved_model = resolved_model.clone();
tauri::async_runtime::spawn(async move {
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&provider_config.default_model)).await;
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model)).await;
// 知识提炼:需读已落库的对话消息,故在 save 之后
if let Err(e) = maybe_spawn_extraction(&session_arc, &db, &conv_id, &provider_config, &knowledge_config, llm_concurrency.clone()).await {
tracing::warn!("知识提炼触发失败(非阻断): {}", e);

View File

@@ -5,6 +5,13 @@ use std::sync::Arc;
use tokio::sync::Mutex;
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider, MessageRole};
// F-01 阶段5: 知识提炼 / 嵌入两路路由。
// - 提炼: TaskRequirements(Standard,无工具)— 在对话 provider 的 model_configs 池中选。
// - 嵌入: TaskRequirements(Capability::Embedding,无工具)— 在 embedding provider 的池中选,
// 池空兜底 config.embedding_model(行为不变,现有 KnowledgeConfig 配置即生效)。
use df_ai::router::{
select_model_id, IntelligenceTier, Modality, TaskRequirements,
};
use df_storage::crud::{AiConversationRepo, KnowledgeRepo};
use df_storage::db::Database;
use df_storage::models::{AiProviderRecord, KnowledgeRecord};
@@ -18,12 +25,19 @@ use crate::commands::err_str;
use super::{AiSession};
/// 按配置构建 embedding provider + model。None = 配置缺失/provider 不存在。
///
/// F-01 阶段5: model 选择路由优先 — 在 embedding provider 的 model_configs 池中按
/// TaskRequirements(needs_tool_use=false)选最优;池空(未拉取)兜底 config.embedding_model。
///
/// 注:TaskRequirements 无 capabilities 字段(router 仅按 needs_tool_use 过滤 ToolUse),
/// 嵌入模型由 embedding provider 的池构成(用户在 KnowledgeSettings 配 embedding provider,
/// 该 provider 的 model_configs 通常即嵌入模型),故 needs_tool_use=false 宽松过滤即可命中。
async fn resolve_embed_provider(
state: &AppState,
config: &crate::state::KnowledgeConfig,
) -> Option<(Box<dyn LlmProvider>, String)> {
let id = config.embedding_provider_id.as_ref()?;
let model = config.embedding_model.clone().unwrap_or_else(|| "embedding-3".to_string());
let fallback_model = config.embedding_model.clone().unwrap_or_else(|| "embedding-3".to_string());
let rec = match state.ai_providers.get_by_id(id).await {
Ok(Some(r)) => r,
_ => {
@@ -39,6 +53,16 @@ async fn resolve_embed_provider(
return None;
}
};
// 嵌入路由:needs_tool_use=false(排除 ToolUse 专要求,允许嵌入模型入选)。
// select_model_id None(池空/无匹配)→ 兜底 config.embedding_model(行为不变)。
let embed_req = TaskRequirements {
modalities: vec![Modality::Text],
needs_tool_use: false,
min_intelligence: IntelligenceTier::Lite,
max_cost: None,
estimated_context: 0,
};
let model = select_model_id(&embed_req, &rec.model_configs).unwrap_or(fallback_model);
Some((provider, model))
}
@@ -314,8 +338,19 @@ async fn extract_knowledge_from_conversation(
ChatMessage::system(EXTRACTION_SYSTEM_PROMPT),
ChatMessage::user(&format!("请从以下对话中提炼可复用知识:\n\n{}", conv_text)),
];
// F-01 阶段5: 知识提炼路由 — TaskRequirements(Standard,无工具)。
// select_model_id None(池空/无匹配)→ 兜底 default_model(行为不变)。
let extract_req = TaskRequirements {
modalities: vec![Modality::Text],
needs_tool_use: false,
min_intelligence: IntelligenceTier::Standard,
max_cost: None,
estimated_context: 0,
};
let extract_model = select_model_id(&extract_req, &provider_config.model_configs)
.unwrap_or_else(|| provider_config.default_model.clone());
let request = CompletionRequest {
model: provider_config.default_model.clone(),
model: extract_model,
messages: extract_messages,
temperature: Some(0.3),
max_tokens: Some(2048),

View File

@@ -6,6 +6,11 @@ use tauri::{AppHandle, Emitter};
use tokio::sync::Mutex;
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider, MessageRole};
// F-01 阶段5: 标题生成路由 — TaskRequirements(Lite + Low 成本,无需工具)。
// 标题是短文本摘要任务,选便宜 Lite 模型;池空/无匹配兜底 default_model(行为不变)。
use df_ai::router::{
select_model_id, CostTier, IntelligenceTier, Modality, TaskRequirements,
};
use df_storage::crud::AiConversationRepo;
use df_storage::db::Database;
use df_storage::models::AiProviderRecord;
@@ -69,7 +74,18 @@ pub(crate) async fn ensure_conversation_title(
return;
}
};
let title = match generate_title_via_llm(&*provider, &provider_config.default_model, summary_msgs, &llm_concurrency).await {
// F-01 阶段5: 标题生成路由 — 选便宜 Lite 模型(Low 成本上限,无工具)。
// select_model_id None(池空/无匹配)→ 兜底 default_model。
let title_req = TaskRequirements {
modalities: vec![Modality::Text],
needs_tool_use: false,
min_intelligence: IntelligenceTier::Lite,
max_cost: Some(CostTier::Low),
estimated_context: 0,
};
let title_model = select_model_id(&title_req, &provider_config.model_configs)
.unwrap_or_else(|| provider_config.default_model.clone());
let title = match generate_title_via_llm(&*provider, &title_model, summary_msgs, &llm_concurrency).await {
Some(t) => t,
None => extract_title(&all_msgs).unwrap_or_else(|| "新对话".to_string()),
};

View File

@@ -187,9 +187,12 @@ pub async fn evaluate_idea(
let scores = df_ideas::scoring::ScoringEngine::compute_default(&idea);
// 对抗式评估(构造注入:从 DB 读默认 provider 装配 LLM无 provider/构造失败 → 启发式兜底)
// F-01 阶段5: 透传 model_configs 池,evaluate_with_llm 经路由选模型(池空兜底 default_model)。
let provider = build_default_provider(&state).await;
let engine = match provider {
Some(p) => df_ideas::adversarial::AdversarialEngine::new(Arc::from(p)),
Some((p, pool)) => {
df_ideas::adversarial::AdversarialEngine::with_pool(Arc::from(p), pool)
}
None => df_ideas::adversarial::AdversarialEngine::heuristic(),
};
let eval = engine.evaluate(&idea).await.map_err(err_str)?;
@@ -262,7 +265,12 @@ pub async fn evaluate_idea(
///
/// 复用 `commands::ai::secret::build_provider_for`resolve→ensure→build 三步),
/// 与 AI Chat / 项目扫描的 provider 构造路径统一FR-S1 密钥解析一致)。
async fn build_default_provider(state: &State<'_, AppState>) -> Option<Box<dyn LlmProvider>> {
///
/// 返回 (provider, model_pool):model_pool = 选中 provider 的 model_configs(F-01 阶段5,
/// 供对抗评估路由)。池空(用户未拉取)→ 调用方兜底 default_model。
async fn build_default_provider(
state: &State<'_, AppState>,
) -> Option<(Box<dyn LlmProvider>, Vec<df_ai::df_ai_core::model::ModelConfig>)> {
let providers = state.ai_providers.list_all().await.ok()?;
let pc = providers
.iter()
@@ -270,7 +278,7 @@ async fn build_default_provider(state: &State<'_, AppState>) -> Option<Box<dyn L
.cloned()
.or_else(|| providers.into_iter().next())?;
match crate::commands::ai::secret::build_provider_for(&pc) {
Ok(p) => Some(p),
Ok(p) => Some((p, pc.model_configs.clone())),
Err(e) => {
// 密钥不可用:启发式兜底,不阻断评估(与 evaluate_idea LLM 失败降级语义一致)
tracing::warn!("默认 provider 密钥不可用,对抗评估走启发式: {e}");

View File

@@ -6,6 +6,11 @@ use serde::{Deserialize, Serialize};
use tauri::State;
use df_ai::provider::{ChatMessage, CompletionRequest};
// F-01 阶段5: 项目扫描描述路由 — TaskRequirements(Standard;采样含图时追加 Vision,
// 当前采样(ProjectSample)未含图像,故仅 Text)。池空/无匹配兜底 default_model。
use df_ai::router::{
select_model_id, IntelligenceTier, Modality, TaskRequirements,
};
use df_core::types::new_id;
use df_project::scan::{
collect_sample, detect_stack, discover_projects, extract_description, is_monorepo,
@@ -522,8 +527,19 @@ async fn extract_description_via_llm(
.map_err(err_str)?
.map_err(err_str)?;
// F-01 阶段5: 项目扫描描述路由 — Standard,无工具;采样未含图故仅 Text。
// select_model_id None(池空/无匹配)→ 兜底 default_model(行为不变)。
let scan_req = TaskRequirements {
modalities: vec![Modality::Text],
needs_tool_use: false,
min_intelligence: IntelligenceTier::Standard,
max_cost: None,
estimated_context: 0,
};
let scan_model = select_model_id(&scan_req, &pc.model_configs)
.unwrap_or_else(|| pc.default_model.clone());
let request = CompletionRequest {
model: pc.default_model.clone(),
model: scan_model,
messages: build_scan_prompt(&sample, &rule_stack),
temperature: Some(0.2),
max_tokens: Some(400),
@@ -606,8 +622,19 @@ pub async fn scan_project_with_ai(
});
}
};
// F-01 阶段5: 项目扫描描述路由 — Standard,无工具;采样未含图故仅 Text。
// select_model_id None(池空/无匹配)→ 兜底 default_model(行为不变)。
let scan_req = TaskRequirements {
modalities: vec![Modality::Text],
needs_tool_use: false,
min_intelligence: IntelligenceTier::Standard,
max_cost: None,
estimated_context: 0,
};
let scan_model = select_model_id(&scan_req, &pc.model_configs)
.unwrap_or_else(|| pc.default_model.clone());
let request = CompletionRequest {
model: pc.default_model.clone(),
model: scan_model,
messages: build_scan_prompt(&sample, &rule_stack),
temperature: Some(0.2),
max_tokens: Some(400),