修复策划 Agent 模型选择链路

保存策划会话的模型目录 ID并交由 api-server 解析

移除官方直连路径的固定模型并补齐 AGC 客户端标记
This commit is contained in:
2026-09-11 05:51:35 +00:00
parent 8444dee9e9
commit f2f786d69b
6 changed files with 37 additions and 11 deletions
@@ -490,6 +490,9 @@ async fn request_design_provider(
}
let config = load_game_creator_app_config()?;
let mut llm = resolve_game_creator_llm_config_for_agent(&config, "design-agent");
if !session.model_id.trim().is_empty() {
llm.model = session.model_id.clone();
}
// 此循环统一处理流中断与 HTTP 瞬态错误,避免与传输重试相乘。
let max_retries = llm.max_retries;
llm.max_retries = 0;
@@ -761,7 +764,10 @@ pub(crate) async fn continue_design_agent_at(
if read_planning_session_v2(root)?.is_some() {
return Err("此项目包含旧策划会话,请查看原有记录或在新项目开始五阶段策划".into());
}
new_design_session(&project_id)
new_design_session(
&project_id,
&load_game_creator_app_config()?.selected_model_id,
)
}
};
if session.project_id != project_id {
@@ -940,7 +946,7 @@ mod tests {
let temp = tempfile::tempdir().expect("tempdir");
let root = temp.path();
concept_artifacts(root);
let mut session = new_design_session("project");
let mut session = new_design_session("project", "quality");
session.turn = Some(DesignTurn {
id: "turn-1".into(),
pending: true,
@@ -987,7 +993,7 @@ mod tests {
#[test]
fn text_cannot_approve_without_event() {
let mut session = new_design_session("project");
let mut session = new_design_session("project", "quality");
let err = prepare_design_input(
&mut session,
"turn-1",
@@ -653,7 +653,7 @@ mod tests {
#[test]
fn phase_context_injects_current_skill_only() {
let resources = DesignResources::new(pack_root()).expect("pack");
let mut session = new_design_session("project");
let mut session = new_design_session("project", "quality");
let concept = resources.phase_context(&session);
assert!(concept.contains("skills.concept"));
assert!(!concept.contains("skills.top_design"));
@@ -677,7 +677,7 @@ mod tests {
copy_dir(&pack_root(), &dest).expect("copy pack");
fs::remove_file(dest.join("resources/skills/concept.md")).expect("remove");
let resources = DesignResources::new(dest).expect("pack without concept skill");
let session = new_design_session("project");
let session = new_design_session("project", "quality");
let context = resources.phase_context(&session);
assert!(!context.contains("skills.concept"));
assert!(context.contains("project/00_concept/design.md"));
@@ -45,6 +45,9 @@ pub(crate) struct DesignSession {
pub(crate) engine: String,
pub(crate) session_id: String,
pub(crate) project_id: String,
/// 入口选择的 AGC 模型目录 ID;同一会话内保持稳定,不保存上游真实模型名。
#[serde(default)]
pub(crate) model_id: String,
pub(crate) current_phase: String,
#[serde(default)]
pub(crate) approved_phases: Vec<String>,
@@ -130,13 +133,17 @@ pub(crate) fn design_phase_required_artifacts() -> BTreeMap<String, Vec<String>>
])
}
pub(crate) fn new_design_session(project_id: impl Into<String>) -> DesignSession {
pub(crate) fn new_design_session(
project_id: impl Into<String>,
model_id: impl Into<String>,
) -> DesignSession {
let now = unix_timestamp();
DesignSession {
schema_version: DESIGN_SESSION_SCHEMA_VERSION.to_string(),
engine: DESIGN_SESSION_ENGINE.to_string(),
session_id: Uuid::new_v4().to_string(),
project_id: project_id.into(),
model_id: model_id.into(),
current_phase: DESIGN_PHASES[0].to_string(),
approved_phases: Vec::new(),
pending_approval: None,
@@ -214,7 +221,7 @@ pub(crate) fn ensure_design_session(
}
return Ok(session);
}
let session = new_design_session(project_id.to_string());
let session = new_design_session(project_id.to_string(), "quality");
write_design_session(root, &session)?;
Ok(session)
}
@@ -321,7 +328,7 @@ mod tests {
#[test]
fn phase_artifact_check_includes_previous_phases() {
let root = tempfile::tempdir().expect("tempdir");
let mut session = new_design_session("project");
let mut session = new_design_session("project", "quality");
fs::create_dir_all(root.path().join(".workspace/project/00_concept")).expect("mkdir");
fs::write(
root.path().join(".workspace/project/00_concept/design.md"),
@@ -347,7 +354,7 @@ mod tests {
#[test]
fn rejecting_approval_keeps_phase_and_clears_request() {
let root = tempfile::tempdir().expect("tempdir");
let mut session = new_design_session("project");
let mut session = new_design_session("project", "quality");
fs::create_dir_all(root.path().join(".workspace/project/00_concept")).expect("mkdir");
fs::write(
root.path().join(".workspace/project/00_concept/design.md"),
@@ -93,7 +93,6 @@ fn user_selected_path_is_authorized(path: &Path, is_directory: bool) -> bool {
}
pub(crate) const OFFICIAL_LLM_ROUTER_BASE_URL: &str = "https://router.genarrative.world/v1";
pub(crate) const OFFICIAL_LLM_ROUTER_MODEL: &str = "gpt-6-astra";
pub(crate) const GAME_CREATOR_LLM_AGENT_REASONING_EFFORT_DEFAULTS: [(&str, &str); 21] = [
(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, "high"),
@@ -208,11 +207,12 @@ fn build_game_creator_official_platform_llm_config(
LlmProvider::OpenAiCompatible,
proxy_base_url,
session.access_token,
OFFICIAL_LLM_ROUTER_MODEL.to_string(),
llm.model.clone(),
llm.request_timeout_ms,
llm.max_retries,
llm.retry_backoff_ms,
)
.map(|config| config.with_agc_client_marker(true))
.map_err(|error| format!("官方 LLM Router 代理配置无效:{error}"))
}
@@ -113,6 +113,8 @@ Runtime 不维护文档版本号,不解析文档版本,不提供版本回退
会话/回合/工具调用身份用于生产恢复和重复请求处理,与 Agent 自行写在策划文档头部的版本号无关。
策划会话在创建时保存入口选择的 AGC 模型目录 ID(例如 `quality``fast`),同一会话后续回合沿用该 ID;客户端不保存或推断上游真实模型名。官方 `platform-llm` 直连 api-server 时携带 AGC 客户端标记,由 api-server 根据模型目录解析实际模型,不能在客户端硬编码某个上游模型替代目录选择。
## 5. Agent Runtime
新的 Runtime 应提供自由工具循环:
+11
View File
@@ -73,6 +73,7 @@ pub struct LlmConfig {
max_retries: u32,
retry_backoff_ms: u64,
official_fallback: bool,
agc_client_marker: bool,
anthropic_strict_tool_support: bool,
openai_chat_token_budget_field: OpenAiChatTokenBudgetField,
}
@@ -1061,6 +1062,7 @@ impl LlmConfig {
max_retries,
retry_backoff_ms,
official_fallback: false,
agc_client_marker: false,
anthropic_strict_tool_support: false,
openai_chat_token_budget_field: OpenAiChatTokenBudgetField::LegacyMaxTokens,
})
@@ -1071,6 +1073,12 @@ impl LlmConfig {
self
}
/// 标记为 AGC 主站客户端请求,使 api-server 按客户端传入的模型目录 ID 解析模型。
pub fn with_agc_client_marker(mut self, enabled: bool) -> Self {
self.agc_client_marker = enabled;
self
}
/// 显式声明当前 Anthropic endpoint 与 model 组合支持 strict tool use。
///
/// 该能力不能由 `api_kind` 推断:旧 Claude 模型和 Anthropic-compatible
@@ -1955,6 +1963,9 @@ impl LlmClient {
);
let mut request_builder = self.http_client.post(url.as_str());
if self.config.agc_client_marker {
request_builder = request_builder.header("x-genarrative-client", "agc");
}
request_builder = match request.api_kind {
LlmApiKind::OpenAiChat | LlmApiKind::OpenAiResponses => {
request_builder.bearer_auth(self.config.api_key())