diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs index a20b961df..2358d1d3a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs @@ -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", diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/design_tools.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/design_tools.rs index edfcb5d7d..b8ff3bdb0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/design_tools.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/design_tools.rs @@ -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")); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/design_session.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/design_session.rs index 79d8e919e..8a6fe20ab 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/design_session.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/design_session.rs @@ -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, @@ -130,13 +133,17 @@ pub(crate) fn design_phase_required_artifacts() -> BTreeMap> ]) } -pub(crate) fn new_design_session(project_id: impl Into) -> DesignSession { +pub(crate) fn new_design_session( + project_id: impl Into, + model_id: impl Into, +) -> 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"), diff --git a/apps/ai-game-creator-shell/src-tauri/src/config.rs b/apps/ai-game-creator-shell/src-tauri/src/config.rs index 8c02b4e6c..b1b27ae90 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/config.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/config.rs @@ -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}")) } diff --git a/docs/technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md b/docs/technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md index fbddd5e89..611bf2a99 100644 --- a/docs/technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md +++ b/docs/technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md @@ -113,6 +113,8 @@ Runtime 不维护文档版本号,不解析文档版本,不提供版本回退 会话/回合/工具调用身份用于生产恢复和重复请求处理,与 Agent 自行写在策划文档头部的版本号无关。 +策划会话在创建时保存入口选择的 AGC 模型目录 ID(例如 `quality`、`fast`),同一会话后续回合沿用该 ID;客户端不保存或推断上游真实模型名。官方 `platform-llm` 直连 api-server 时携带 AGC 客户端标记,由 api-server 根据模型目录解析实际模型,不能在客户端硬编码某个上游模型替代目录选择。 + ## 5. Agent Runtime 新的 Runtime 应提供自由工具循环: diff --git a/server-rs/crates/platform-llm/src/lib.rs b/server-rs/crates/platform-llm/src/lib.rs index 80382e778..b3a7429ca 100644 --- a/server-rs/crates/platform-llm/src/lib.rs +++ b/server-rs/crates/platform-llm/src/lib.rs @@ -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())