diff --git a/apps/ai-game-creator-shell/src-tauri/src/llm_catalog.rs b/apps/ai-game-creator-shell/src-tauri/src/llm_catalog.rs index 7e89cb64f..620701eb0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/llm_catalog.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/llm_catalog.rs @@ -6,40 +6,12 @@ use futures::StreamExt; use serde::Deserialize; use serde_json::Value; use shared_contracts::api::API_RESPONSE_ENVELOPE_HEADER; -use shared_contracts::llm::{LlmModelSummary, LlmModelsResponse}; +use shared_contracts::llm::LlmModelsResponse; use std::time::Duration; const API_RESPONSE_ENVELOPE_VERSION: &str = "v1"; const MODEL_CATALOG_MAX_BYTES: usize = 1024 * 1024; -fn default_codex_agent_mode() -> String { - "codex".to_string() -} - -fn default_model_protocol() -> String { - "openai_responses".to_string() -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct WireModelSummary { - id: String, - display_name: String, - #[serde(default = "default_codex_agent_mode")] - agent_mode: String, - /// 线协议与执行器正交;旧服务端不下发时按 OpenAI Responses 兼容。 - #[serde(default = "default_model_protocol")] - protocol: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct WireModelCatalog { - default_model_id: String, - models: Vec, - revision: u64, -} - #[derive(Debug, Deserialize)] struct ApiErrorPayload { code: Option, @@ -49,27 +21,12 @@ struct ApiErrorPayload { #[derive(Debug, Deserialize)] struct ApiEnvelope { ok: bool, - data: Option, + /// 直接复用 shared-contracts 的目录 DTO:`agentMode` / `protocol` / `unavailableModels` + /// 都带 `#[serde(default)]`,旧服务端缺字段时按 Codex + OpenAI Responses + 空桶兼容。 + data: Option, error: Option, } -fn map_catalog(catalog: WireModelCatalog) -> LlmModelsResponse { - LlmModelsResponse { - default_model_id: catalog.default_model_id, - models: catalog - .models - .into_iter() - .map(|model| LlmModelSummary { - id: model.id, - display_name: model.display_name, - agent_mode: model.agent_mode, - protocol: model.protocol, - }) - .collect(), - revision: catalog.revision, - } -} - fn parse_catalog_payload(bytes: &[u8]) -> Result { let value: Value = serde_json::from_slice(bytes) .map_err(|_| "模型列表响应不是有效 JSON,请稍后重试".to_string())?; @@ -98,12 +55,10 @@ fn parse_catalog_payload(bytes: &[u8]) -> Result { } return envelope .data - .map(map_catalog) .ok_or_else(|| "模型列表响应缺少 data,请稍后重试".to_string()); } - serde_json::from_value::(value) - .map(map_catalog) + serde_json::from_value::(value) .map_err(|_| "模型列表响应格式无效,请稍后重试".to_string()) } @@ -293,6 +248,48 @@ mod tests { .contains("x-genarrative-response-envelope: v1")); } + #[test] + fn catalog_maps_unknown_unavailable_reason_to_unknown() { + let body = serde_json::json!({ + "ok": true, + "data": { + "defaultModelId": "quality", + "models": [{ + "id": "quality", + "displayName": "高质量", + "agentMode": "codex", + "protocol": "openai_responses" + }], + "unavailableModels": [{ + "id": "pro", + "displayName": "专业版", + "agentMode": "cc", + "protocol": "anthropic", + "reason": "some_future_reason" + }], + "revision": 8 + }, + "error": null + }) + .to_string(); + + let catalog = parse_catalog_payload(body.as_bytes()).expect("catalog should parse"); + assert_eq!(catalog.models.len(), 1); + assert_eq!(catalog.unavailable_models.len(), 1); + assert_eq!( + catalog.unavailable_models[0].reason, + shared_contracts::llm::LlmModelUnavailableReason::Unknown + ); + assert_eq!( + catalog.unavailable_models[0].agent_mode, + shared_contracts::llm::AgcAgentMode::Cc + ); + assert_eq!( + catalog.unavailable_models[0].protocol, + shared_contracts::llm::AgcModelProtocol::Anthropic + ); + } + #[tokio::test] async fn official_catalog_fails_closed_on_http_error_without_accepting_error_as_data() { let listener = TcpListener::bind("127.0.0.1:0").expect("bind fixture");