feat: AGC 支持模型方案选择
新增 Router 模型目录接口并返回可用模型列表 客户端配置新增模型方案与当前选中方案字段 设置页新增模型方案卡片和管理弹层,支持方案名称、模型和推理强度 自定义下拉菜单圆角、遮罩层级与窗口边界定位,并隐藏滚动条 AGC 官方路由保留所选模型并携带客户端标记,由服务端校验后转发 补齐配置结构变更后的既有测试初始化字段
This commit is contained in:
@@ -1776,7 +1776,9 @@ impl CodexAppServerConnection {
|
||||
effective_llm.base_url =
|
||||
format!("{}/api/llm", session.api_base_url.trim_end_matches('/'));
|
||||
effective_llm.api_key.clear();
|
||||
effective_llm.model = OFFICIAL_LLM_ROUTER_MODEL.to_string();
|
||||
// The selected model profile is resolved and validated at config
|
||||
// load time; the official route still owns the provider/base URL.
|
||||
effective_llm.model = llm.model.clone();
|
||||
CodexAppServerCredential::PlatformSession {
|
||||
fingerprint: format!(
|
||||
"platform-session:{}:{}:{}",
|
||||
|
||||
@@ -137,6 +137,10 @@ async fn proxy_codex_provider_request(
|
||||
headers.append(name.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
headers.insert(
|
||||
axum::http::HeaderName::from_static("x-genarrative-client"),
|
||||
axum::http::HeaderValue::from_static("agc"),
|
||||
);
|
||||
let upstream_authorization = match format!("Bearer {}", state.upstream_bearer_token).parse() {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
|
||||
@@ -3482,7 +3482,23 @@ pub(crate) fn lock_game_creator_app_config_to_official_route(config: &mut GameCr
|
||||
config.agent_mode = GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER.to_string();
|
||||
config.llm.api_key.clear();
|
||||
config.llm.base_url = OFFICIAL_LLM_ROUTER_BASE_URL.to_string();
|
||||
config.llm.model = OFFICIAL_LLM_ROUTER_MODEL.to_string();
|
||||
if let Some(profile) = config
|
||||
.model_profiles
|
||||
.iter()
|
||||
.find(|profile| profile.enabled && profile.id == config.selected_model_profile_id)
|
||||
{
|
||||
if !profile.model_id.trim().is_empty() {
|
||||
config.llm.model = profile.model_id.trim().to_string();
|
||||
}
|
||||
if let Some(reasoning_effort) = profile.reasoning_effort.as_deref() {
|
||||
if !reasoning_effort.trim().is_empty() {
|
||||
config.llm.reasoning_effort = reasoning_effort.trim().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
if config.llm.model.trim().is_empty() {
|
||||
config.llm.model = OFFICIAL_LLM_ROUTER_MODEL.to_string();
|
||||
}
|
||||
config.llm.api_kind = DEFAULT_GAME_CREATOR_LLM_API_KIND.to_string();
|
||||
config.agent_llm.clear();
|
||||
config.editor_api.api_key.clear();
|
||||
@@ -3616,6 +3632,19 @@ pub(crate) fn merge_game_creator_config_file(
|
||||
config.planning.capability_enabled = capability_enabled;
|
||||
}
|
||||
}
|
||||
if let Some(model_profiles) = file_config.model_profiles {
|
||||
config.model_profiles = model_profiles
|
||||
.into_iter()
|
||||
.filter(|profile| {
|
||||
!profile.id.trim().is_empty()
|
||||
&& !profile.name.trim().is_empty()
|
||||
&& !profile.model_id.trim().is_empty()
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
if let Some(selected_model_profile_id) = file_config.selected_model_profile_id {
|
||||
config.selected_model_profile_id = selected_model_profile_id;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1020,6 +1020,26 @@ struct GameCreatorAppConfigFile {
|
||||
agent_llm: Option<BTreeMap<String, GameCreatorLlmConfigFile>>,
|
||||
editor_api: Option<GameCreatorEditorApiConfigFile>,
|
||||
planning: Option<GameCreatorPlanningConfigFile>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
model_profiles: Option<Vec<GameCreatorModelProfileFile>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
selected_model_profile_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GameCreatorModelProfileFile {
|
||||
id: String,
|
||||
name: String,
|
||||
model_id: String,
|
||||
#[serde(default = "default_model_profile_enabled")]
|
||||
enabled: bool,
|
||||
#[serde(default)]
|
||||
reasoning_effort: Option<String>,
|
||||
}
|
||||
|
||||
fn default_model_profile_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
@@ -1080,6 +1100,14 @@ struct GameCreatorAppConfig {
|
||||
editor_api: GameCreatorEditorApiConfig,
|
||||
#[serde(default)]
|
||||
planning: GameCreatorPlanningConfig,
|
||||
#[serde(default)]
|
||||
model_profiles: Vec<GameCreatorModelProfileFile>,
|
||||
#[serde(default = "default_selected_model_profile_id")]
|
||||
selected_model_profile_id: String,
|
||||
}
|
||||
|
||||
fn default_selected_model_profile_id() -> String {
|
||||
"default".to_string()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
@@ -1606,6 +1634,14 @@ impl Default for GameCreatorAppConfig {
|
||||
agent_llm: BTreeMap::new(),
|
||||
editor_api: GameCreatorEditorApiConfig::default(),
|
||||
planning: GameCreatorPlanningConfig::default(),
|
||||
model_profiles: vec![GameCreatorModelProfileFile {
|
||||
id: "default".to_string(),
|
||||
name: "陶泥儿智能创作".to_string(),
|
||||
model_id: OFFICIAL_LLM_ROUTER_MODEL.to_string(),
|
||||
enabled: true,
|
||||
reasoning_effort: Some("max".to_string()),
|
||||
}],
|
||||
selected_model_profile_id: default_selected_model_profile_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,6 +737,8 @@ fn app_config_commands_write_runtime_config_file() {
|
||||
},
|
||||
agent_llm,
|
||||
planning: GameCreatorPlanningConfig::default(),
|
||||
model_profiles: Vec::new(),
|
||||
selected_model_profile_id: "default".to_string(),
|
||||
})
|
||||
.expect("write runtime config");
|
||||
|
||||
@@ -822,6 +824,8 @@ fn app_config_write_rejects_invalid_api_kind() {
|
||||
editor_api: GameCreatorEditorApiConfig::default(),
|
||||
agent_llm: BTreeMap::new(),
|
||||
planning: GameCreatorPlanningConfig::default(),
|
||||
model_profiles: Vec::new(),
|
||||
selected_model_profile_id: "default".to_string(),
|
||||
});
|
||||
|
||||
assert!(result
|
||||
@@ -847,6 +851,8 @@ fn app_config_write_rejects_invalid_reasoning_effort() {
|
||||
editor_api: GameCreatorEditorApiConfig::default(),
|
||||
agent_llm: BTreeMap::new(),
|
||||
planning: GameCreatorPlanningConfig::default(),
|
||||
model_profiles: Vec::new(),
|
||||
selected_model_profile_id: "default".to_string(),
|
||||
});
|
||||
|
||||
assert!(result
|
||||
@@ -872,6 +878,8 @@ fn app_config_write_rejects_too_small_request_timeout() {
|
||||
editor_api: GameCreatorEditorApiConfig::default(),
|
||||
agent_llm: BTreeMap::new(),
|
||||
planning: GameCreatorPlanningConfig::default(),
|
||||
model_profiles: Vec::new(),
|
||||
selected_model_profile_id: "default".to_string(),
|
||||
});
|
||||
|
||||
assert!(result
|
||||
|
||||
@@ -718,11 +718,21 @@ export interface GameCreatorAppConfig {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
};
|
||||
modelProfiles: GameCreatorModelProfile[];
|
||||
selectedModelProfileId: string;
|
||||
planning?: {
|
||||
capabilityEnabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GameCreatorModelProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
modelId: string;
|
||||
enabled: boolean;
|
||||
reasoningEffort: GameCreatorLlmReasoningEffort;
|
||||
}
|
||||
|
||||
export interface GameCreatorAppConfigView {
|
||||
path: string;
|
||||
config: GameCreatorAppConfig;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -179,6 +179,19 @@ export type ClientEditorAssetLibrary = {
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ClientLlmModel = {
|
||||
id: string;
|
||||
capabilities?: string[];
|
||||
};
|
||||
|
||||
export function loadClientLlmModels() {
|
||||
return requestClientApi<{ models: ClientLlmModel[] }>(
|
||||
'/api/llm/models',
|
||||
{ method: 'GET' },
|
||||
'读取可用模型失败',
|
||||
).then((response) => response.models ?? []);
|
||||
}
|
||||
|
||||
export function loadEditorAssetLibrary() {
|
||||
return requestClientApi<{ library: ClientEditorAssetLibrary }>(
|
||||
'/api/editor/assets/library',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user