修复策划 Agent 模型选择链路
保存策划会话的模型目录 ID并交由 api-server 解析 移除官方直连路径的固定模型并补齐 AGC 客户端标记
This commit is contained in:
@@ -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 应提供自由工具循环:
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user