默认使用 Responses 协议
将 platform-llm 文本请求默认协议改为 Responses 为旧 Chat Completions 网关保留显式兼容开关 同步 AI 游戏创作 App 协议配置与文档
This commit is contained in:
@@ -336,6 +336,7 @@ for (const snippet of [
|
||||
'function writeStreamingChatCompletion',
|
||||
'requestJson?.stream === true',
|
||||
`requestBodies.every((body) => body.includes('"stream":true'))`,
|
||||
"GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL: 'chat_completions'",
|
||||
"GENARRATIVE_GAME_CREATOR_LLM_STREAM: 'true'",
|
||||
"method: 'HEAD'",
|
||||
'previewAssetHead.contentLength === String(smokeAssetBytes.length)',
|
||||
|
||||
@@ -573,6 +573,7 @@ function runAgent(baseUrl) {
|
||||
GENARRATIVE_GAME_CREATOR_LLM_API_KEY: 'local-provider-key',
|
||||
GENARRATIVE_GAME_CREATOR_LLM_BASE_URL: baseUrl,
|
||||
GENARRATIVE_GAME_CREATOR_LLM_MODEL: 'local-game-creator-smoke',
|
||||
GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL: 'chat_completions',
|
||||
GENARRATIVE_GAME_CREATOR_LLM_STREAM: 'true',
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
|
||||
@@ -14,7 +14,8 @@ use platform_agent::{
|
||||
route_game_creation_repair_issues,
|
||||
};
|
||||
use platform_llm::{
|
||||
LlmClient, LlmConfig, LlmMessage, LlmProvider, LlmTextRequest, DEFAULT_RETRY_BACKOFF_MS,
|
||||
LlmClient, LlmConfig, LlmMessage, LlmProvider, LlmTextProtocol, LlmTextRequest,
|
||||
DEFAULT_RETRY_BACKOFF_MS,
|
||||
};
|
||||
use reqwest::header;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -90,6 +91,7 @@ struct GameCreatorLlmConfigStatus {
|
||||
api_key_present: bool,
|
||||
base_url: Option<String>,
|
||||
model: Option<String>,
|
||||
protocol: String,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1343,6 +1345,26 @@ fn build_game_creator_llm_client_from_env() -> Result<LlmClient, String> {
|
||||
LlmClient::new(config).map_err(|error| format!("LLM client 初始化失败:{error}"))
|
||||
}
|
||||
|
||||
fn read_game_creator_llm_protocol_from_env() -> Result<LlmTextProtocol, String> {
|
||||
match read_first_non_empty_env(&[
|
||||
"GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL",
|
||||
"GENARRATIVE_LLM_PROTOCOL",
|
||||
"LLM_PROTOCOL",
|
||||
])
|
||||
.unwrap_or_else(|| "responses".to_string())
|
||||
.trim()
|
||||
.to_ascii_lowercase()
|
||||
.replace('-', "_")
|
||||
.as_str()
|
||||
{
|
||||
"responses" | "response" | "responses_api" => Ok(LlmTextProtocol::Responses),
|
||||
"chat_completions" | "chat_completion" | "chat" => Ok(LlmTextProtocol::ChatCompletions),
|
||||
value => Err(format!(
|
||||
"LLM protocol 无效:{value},请使用 responses 或 chat_completions"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_game_creator_llm_config_from_env() -> GameCreatorLlmConfigStatus {
|
||||
let local_env_error = load_game_creator_local_env().err();
|
||||
let api_key = read_first_non_empty_env(&[
|
||||
@@ -1365,6 +1387,13 @@ fn check_game_creator_llm_config_from_env() -> GameCreatorLlmConfigStatus {
|
||||
]);
|
||||
let mut status =
|
||||
check_game_creator_llm_config_values(api_key.clone(), base_url.clone(), model.clone());
|
||||
status.protocol = read_game_creator_llm_protocol_from_env()
|
||||
.map(game_creator_llm_protocol_name)
|
||||
.unwrap_or_else(|error| {
|
||||
status.configured = false;
|
||||
status.error = Some(error);
|
||||
"responses".to_string()
|
||||
});
|
||||
if let Some(error) = local_env_error {
|
||||
status.configured = false;
|
||||
status.error = Some(error);
|
||||
@@ -1419,10 +1448,19 @@ fn check_game_creator_llm_config_values(
|
||||
api_key_present,
|
||||
base_url,
|
||||
model,
|
||||
protocol: "responses".to_string(),
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
fn game_creator_llm_protocol_name(protocol: LlmTextProtocol) -> String {
|
||||
match protocol {
|
||||
LlmTextProtocol::ChatCompletions => "chat_completions",
|
||||
LlmTextProtocol::Responses => "responses",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
struct AgentProgressEmitter<'a> {
|
||||
app: &'a tauri::AppHandle,
|
||||
project_path: String,
|
||||
@@ -1800,6 +1838,7 @@ async fn request_planner_spec_with_client(
|
||||
long_memory,
|
||||
)),
|
||||
])
|
||||
.with_protocol(read_game_creator_llm_protocol_from_env()?)
|
||||
.with_max_tokens(GAME_CREATOR_PLANNER_MAX_OUTPUT_TOKENS);
|
||||
let response = request_game_creator_llm_text(client, request)
|
||||
.await
|
||||
@@ -1843,6 +1882,7 @@ async fn request_generator_game_draft_with_client(
|
||||
LlmMessage::system(system_prompt),
|
||||
LlmMessage::user(user_prompt.clone()),
|
||||
])
|
||||
.with_protocol(read_game_creator_llm_protocol_from_env()?)
|
||||
.with_max_tokens(GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS);
|
||||
match request_game_creator_llm_text(client, request).await {
|
||||
Ok(response) => break response,
|
||||
@@ -6135,6 +6175,7 @@ fn run_cli_command(command: CliCommand) -> Result<(), String> {
|
||||
println!("llm.apiKeyPresent={}", status.api_key_present);
|
||||
println!("llm.baseUrl={}", status.base_url.unwrap_or_default());
|
||||
println!("llm.model={}", status.model.unwrap_or_default());
|
||||
println!("llm.protocol={}", status.protocol);
|
||||
if let Some(error) = status.error {
|
||||
println!("llm.error={error}");
|
||||
}
|
||||
@@ -6317,6 +6358,7 @@ mod tests {
|
||||
GENARRATIVE_GAME_CREATOR_LLM_API_KEY=file-key
|
||||
GENARRATIVE_GAME_CREATOR_LLM_BASE_URL="https://example.test/v1"
|
||||
export GENARRATIVE_GAME_CREATOR_LLM_MODEL='model-from-file'
|
||||
GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL=chat_completions
|
||||
GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
"#,
|
||||
)
|
||||
@@ -6325,10 +6367,12 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
let api_key = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_API_KEY").ok();
|
||||
let base_url = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL").ok();
|
||||
let model = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_MODEL").ok();
|
||||
let protocol = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL").ok();
|
||||
let stream = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_STREAM").ok();
|
||||
std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_API_KEY", "process-key");
|
||||
std::env::remove_var("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL");
|
||||
std::env::remove_var("GENARRATIVE_GAME_CREATOR_LLM_MODEL");
|
||||
std::env::remove_var("GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL");
|
||||
std::env::remove_var("GENARRATIVE_GAME_CREATOR_LLM_STREAM");
|
||||
|
||||
load_game_creator_env_file(&env_path).expect("load local env");
|
||||
@@ -6345,6 +6389,10 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
std::env::var("GENARRATIVE_GAME_CREATOR_LLM_MODEL").as_deref(),
|
||||
Ok("model-from-file")
|
||||
);
|
||||
assert_eq!(
|
||||
std::env::var("GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL").as_deref(),
|
||||
Ok("chat_completions")
|
||||
);
|
||||
assert_eq!(
|
||||
std::env::var("GENARRATIVE_GAME_CREATOR_LLM_STREAM").as_deref(),
|
||||
Ok("true")
|
||||
@@ -6353,6 +6401,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
restore_env("GENARRATIVE_GAME_CREATOR_LLM_API_KEY", api_key);
|
||||
restore_env("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", base_url);
|
||||
restore_env("GENARRATIVE_GAME_CREATOR_LLM_MODEL", model);
|
||||
restore_env("GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL", protocol);
|
||||
restore_env("GENARRATIVE_GAME_CREATOR_LLM_STREAM", stream);
|
||||
fs::remove_dir_all(root).expect("cleanup test env dir");
|
||||
}
|
||||
@@ -6580,17 +6629,28 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
let _ = sender
|
||||
.send(String::from_utf8_lossy(&request_buffer[..read_len]).into_owned());
|
||||
}
|
||||
let body = serde_json::json!({
|
||||
"id": "chatcmpl_game_creator_mock",
|
||||
"model": "mock-game-model",
|
||||
"choices": [
|
||||
{
|
||||
"message": { "content": response_content },
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"usage": { "prompt_tokens": 11, "completion_tokens": 22, "total_tokens": 33 }
|
||||
})
|
||||
let request_text = String::from_utf8_lossy(&request_buffer[..read_len]);
|
||||
let body = if request_text.contains("POST /responses HTTP/1.1") {
|
||||
serde_json::json!({
|
||||
"id": "resp_game_creator_mock",
|
||||
"model": "mock-game-model",
|
||||
"output_text": response_content,
|
||||
"status": "completed",
|
||||
"usage": { "input_tokens": 11, "output_tokens": 22, "total_tokens": 33 }
|
||||
})
|
||||
} else {
|
||||
serde_json::json!({
|
||||
"id": "chatcmpl_game_creator_mock",
|
||||
"model": "mock-game-model",
|
||||
"choices": [
|
||||
{
|
||||
"message": { "content": response_content },
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"usage": { "prompt_tokens": 11, "completion_tokens": 22, "total_tokens": 33 }
|
||||
})
|
||||
}
|
||||
.to_string();
|
||||
let response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
@@ -6741,15 +6801,18 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
let previous_api_key = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_API_KEY").ok();
|
||||
let previous_base_url = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL").ok();
|
||||
let previous_model = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_MODEL").ok();
|
||||
let previous_protocol = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL").ok();
|
||||
std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_API_KEY", "test-key");
|
||||
std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", base_url);
|
||||
std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_MODEL", "mock-game-model");
|
||||
std::env::remove_var("GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL");
|
||||
|
||||
let result = generate_local_game_draft_at(&root, "用上传角色图做主角", None).await;
|
||||
|
||||
restore_env("GENARRATIVE_GAME_CREATOR_LLM_API_KEY", previous_api_key);
|
||||
restore_env("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", previous_base_url);
|
||||
restore_env("GENARRATIVE_GAME_CREATOR_LLM_MODEL", previous_model);
|
||||
restore_env("GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL", previous_protocol);
|
||||
result.expect("generated draft");
|
||||
|
||||
let requests = receiver.try_iter().collect::<Vec<_>>();
|
||||
@@ -6780,6 +6843,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
Some("http://127.0.0.1:1/v1")
|
||||
);
|
||||
assert_eq!(configured.model.as_deref(), Some("mock-game-model"));
|
||||
assert_eq!(configured.protocol, "responses");
|
||||
assert!(!serde_json::to_string(&configured)
|
||||
.unwrap()
|
||||
.contains("unit-test-api-key"));
|
||||
@@ -7256,9 +7320,11 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
let previous_api_key = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_API_KEY").ok();
|
||||
let previous_base_url = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL").ok();
|
||||
let previous_model = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_MODEL").ok();
|
||||
let previous_protocol = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL").ok();
|
||||
std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_API_KEY", "test-key");
|
||||
std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", base_url);
|
||||
std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_MODEL", "mock-game-model");
|
||||
std::env::remove_var("GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL");
|
||||
|
||||
let error = generate_local_game_draft_at(&root, "做一个会失败三轮的厨房游戏", None)
|
||||
.await
|
||||
@@ -7267,6 +7333,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
restore_env("GENARRATIVE_GAME_CREATOR_LLM_API_KEY", previous_api_key);
|
||||
restore_env("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", previous_base_url);
|
||||
restore_env("GENARRATIVE_GAME_CREATOR_LLM_MODEL", previous_model);
|
||||
restore_env("GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL", previous_protocol);
|
||||
assert!(error.contains("已重试"));
|
||||
assert!(error.contains(&GAME_CREATOR_AGENT_LOOP_MAX_PASSES.to_string()));
|
||||
assert!(!root.join("memory/session.md").exists());
|
||||
|
||||
@@ -59,6 +59,7 @@ interface GameCreatorLlmConfigStatus {
|
||||
apiKeyPresent: boolean;
|
||||
baseUrl: string | null;
|
||||
model: string | null;
|
||||
protocol: string;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
@@ -1613,7 +1614,7 @@ export function App() {
|
||||
text: status.configured
|
||||
? `LLM 已配置:${status.model ?? '未命名模型'} @ ${
|
||||
status.baseUrl ?? '未设置 base_url'
|
||||
},API Key 已读取。`
|
||||
},${status.protocol},API Key 已读取。`
|
||||
: `LLM 未就绪:${status.error ?? '配置不完整'}。API Key:${
|
||||
status.apiKeyPresent ? '已读取' : '未读取'
|
||||
}。`,
|
||||
|
||||
@@ -1376,6 +1376,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
apiKeyPresent: true,
|
||||
baseUrl: 'https://llm.example.test/v1',
|
||||
model: 'gpt-test',
|
||||
protocol: 'responses',
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
@@ -1388,7 +1389,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'LLM 已配置:gpt-test @ https://llm.example.test/v1,API Key 已读取。',
|
||||
'LLM 已配置:gpt-test @ https://llm.example.test/v1,responses,API Key 已读取。',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(screen.queryByText(/sk-test-secret/)).toBeNull();
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
- 验证方式:运行 `cargo test -p platform-llm --manifest-path server-rs/Cargo.toml request_text_parses_non_stream_response`,并用真实 OpenAI-compatible 环境变量执行 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-real-loop-test-6 "做一个像素风反弹弹幕厨房小游戏..."`,确认 36 个 trace step、36 次 tool call、`game.static_smoke`、`preview.start` 和 `preview.stop` 完成。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
2026-06-27 追加:`platform-llm` 的 `LlmTextRequest::new` 默认协议改为 Responses;旧 `/api/llm/chat/completions` 代理、RPG runtime chat 和需要旧测试网关的 AI 游戏创作 smoke 必须显式选择 Chat Completions。AI 游戏创作真实 LLM 默认 Responses,可用 `GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL=chat_completions` 兼容旧 OpenAI Chat Completions 网关。
|
||||
|
||||
## 2026-06-24 AI 游戏创作 App 生成编排使用文件驱动 loop
|
||||
|
||||
- 背景:AI 游戏创作 App 的 `game.generate_draft` 已接入 LLM,但单次请求仍不能体现 Planner / Generator / Evaluator 的协作闭环,也无法把评估反馈作为下一轮生成输入。
|
||||
|
||||
@@ -94,7 +94,7 @@ game-project/
|
||||
- `npm run check:native-shells`:覆盖 AI 游戏创作壳的 release/dev 窗口边界、正式用户 App 不嵌入游戏预览 iframe、用户侧预览命令交给外部浏览器和 Tauri release `--no-bundle` 构建 smoke;用于证明正式用户窗口只登记 `main` 聊天窗口,开发面板只在 debug/dev 路径打开,独立壳能完成 release 编译。
|
||||
- `npm run check:encoding` 与 `git diff --check`:覆盖中文文档、中文命令文案和补丁空白;用于避免乱码、尾随空白和无关格式漂移。
|
||||
- `npm run ai-game-creator-shell:llm-status`:只检查 LLM 环境变量是否就绪,不请求上游、不显示 API Key;用于本机联调前确认配置。CLI 和桌面 App 内的 `/llm-status` / 生成入口都会先读取仓库根目录或 `apps/ai-game-creator-shell/` 下 gitignored 的 `.env.secrets.local`,再检查当前进程环境。
|
||||
- `npm run ai-game-creator-shell:agent-run -- --no-wait /绝对项目路径 "游戏创作需求"`:使用真实 OpenAI-compatible 配置跑一次本地生成、落盘、自检和预览;用于人工验收真实 provider 路径。真实 provider 可放在 gitignored 的 `.env.secrets.local` 中,至少包含 `GENARRATIVE_GAME_CREATOR_LLM_API_KEY`、`GENARRATIVE_GAME_CREATOR_LLM_BASE_URL`、`GENARRATIVE_GAME_CREATOR_LLM_MODEL`,真实网关长请求若在非流式响应前被 60 秒空闲连接切断,联调时设置 `GENARRATIVE_GAME_CREATOR_LLM_STREAM=true`。
|
||||
- `npm run ai-game-creator-shell:agent-run -- --no-wait /绝对项目路径 "游戏创作需求"`:使用真实 OpenAI-compatible 配置跑一次本地生成、落盘、自检和预览;用于人工验收真实 provider 路径。真实 provider 可放在 gitignored 的 `.env.secrets.local` 中,至少包含 `GENARRATIVE_GAME_CREATOR_LLM_API_KEY`、`GENARRATIVE_GAME_CREATOR_LLM_BASE_URL`、`GENARRATIVE_GAME_CREATOR_LLM_MODEL`;默认按 Responses 协议请求,旧 Chat Completions 兼容网关需显式设置 `GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL=chat_completions`;真实网关长请求若在非流式响应前被 60 秒空闲连接切断,联调时设置 `GENARRATIVE_GAME_CREATOR_LLM_STREAM=true`。
|
||||
|
||||
## 当前最小落地
|
||||
|
||||
@@ -104,12 +104,12 @@ game-project/
|
||||
- 普通用户侧的生成、上传、运行、自检、预览状态 / 启动 / 打开 / 停止、记忆写入和画板资产导入都必须先完成 `/project` 初始化;未初始化时只提示设置本地项目,不落到默认临时目录。
|
||||
- 终端可用 `npm run ai-game-creator-shell:llm-status` 检查 LLM 环境变量是否就绪;CLI 和桌面 App 内的 `/llm-status` / 生成入口都会先读取 gitignored 的 `.env.secrets.local`,不请求上游、不显示 API Key,缺配置时以非零状态退出或在聊天里提示未就绪。
|
||||
- 终端可用 `npm run ai-game-creator-shell:check` 跑 v1 开发验收:壳 typecheck、`platform-agent` 编排测试、共享契约测试、Tauri Rust 测试和无密钥本地 provider 端到端 smoke。
|
||||
- 终端可用 `npm run ai-game-creator-shell:agent-run -- /绝对项目路径 "游戏创作需求"` 跑一次真实 LLM 生成、落盘、`game.static_smoke` 和本地 HTTP 预览;该入口读取当前环境和 gitignored 的 `.env.secrets.local`,不把 API Key 写入仓库或项目文件。自动验证可加 `--no-wait`,例如 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-test "像素风反弹弹幕厨房"`,生成预览 trace 后立即停止本地预览,避免终端卡在回车等待。真实 OpenAI-compatible 网关建议设置 `GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 跑 Planner 和 Generator,避免长请求非流式空闲断连。
|
||||
- 终端可用 `npm run ai-game-creator-shell:agent-run -- /绝对项目路径 "游戏创作需求"` 跑一次真实 LLM 生成、落盘、`game.static_smoke` 和本地 HTTP 预览;该入口读取当前环境和 gitignored 的 `.env.secrets.local`,不把 API Key 写入仓库或项目文件。自动验证可加 `--no-wait`,例如 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-test "像素风反弹弹幕厨房"`,生成预览 trace 后立即停止本地预览,避免终端卡在回车等待。默认协议为 Responses;旧 Chat Completions 兼容网关设置 `GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL=chat_completions`。真实 OpenAI-compatible 网关建议设置 `GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 跑 Planner 和 Generator,避免长请求非流式空闲断连。
|
||||
- 终端可用 `npm run ai-game-creator-shell:agent-run:smoke` 跑一次无密钥本地端到端 smoke:脚本启动本机 OpenAI-compatible SSE 流式测试 provider,预置一个本地上传图片和一个本地上传音频,复用真实 `--agent-run`、Planner / Orchestrator / 角色 agent / Generator / Evaluator loop、本地落盘、`game.static_smoke` 和本地 HTTP 预览,并断言每次 provider 请求都使用 `stream: true`、provider prompt 收到图片与音频资产上下文、生成 HTML 引用这些资产、预览服务能用 `GET` 读取 `/assets/...`、用 `HEAD` 返回真实资源长度和对应 MIME、headless Chrome 打开预览后至少执行一帧游戏 JS,且通过确定性亮色探针采样证明 canvas 不是空白画布、`.agent/run.latest.json` 的 step group 覆盖 design / balance / art / audio / code / publishing 六组、第二轮会重跑 Evaluator 命中任务及其下游影响任务,未受影响角色 carry-over;随后脚本自动给 CLI 发送回车停止预览。该脚本只用于开发验证,不进入产品生成路径。
|
||||
- `npm run ai-game-creator-shell:dev` 的 Tauri `devUrl` 固定为 `http://127.0.0.1:3080/`,Vite 必须 `strictPort` 对齐;`beforeDevCommand` 先复用已经跑在 3080 且页面标题为 `AI 游戏创作` 的本 app Vite server,否则才启动新的 Vite,若端口被其它服务占用则直接失败并提示释放端口。
|
||||
- `.agent/manifest.json` 会保存 6 个专业组下 16 个组内角色任务状态,当前覆盖 `Director`、`Gameplay`、`Difficulty`、`Asset`、`Polish`、`SFX`、`Code`、`Review`、`Preview`、`Playtest`、`Publish`;程序组内显式包含 `quality-review` 质量评审 gate,由 Evaluator trace 标记完成;开发窗口的专业组面板读取 manifest,而不是前端硬编码。
|
||||
- 共享契约和 `platform-agent` 会按任务依赖与 `completed` 状态计算当前可执行任务,作为 v1 的最小编排选择器;每轮 `Orchestrator` 的 activeTaskIds、carriedTaskIds、repairRoutes 和 dependencyWaves 由 `platform-agent` 纯编排内核产出,`apps/ai-game-creator-shell` 只负责写入 `.agent/passes/pass-N/` 和执行本地工具;`Evaluator` 会在 `.agent/findings.md` 写出 `## Repair Routes` JSON,下一轮编排优先采用该结构化 taskIds,解析不到时才退回关键词路由;返工路由会按任务图自动扩展下游影响任务,例如美术资产变化会继续触发程序预览和运营包装重算。
|
||||
- `game.generate_draft` 使用 OpenAI-compatible LLM 配置生成结构化 JSON 草案,读取 `GENARRATIVE_GAME_CREATOR_LLM_API_KEY` / `GENARRATIVE_LLM_API_KEY` / `LLM_API_KEY` / `OPENAI_API_KEY`、`GENARRATIVE_GAME_CREATOR_LLM_BASE_URL` / `GENARRATIVE_LLM_BASE_URL` / `LLM_BASE_URL` / `OPENAI_BASE_URL`、`GENARRATIVE_GAME_CREATOR_LLM_MODEL` / `GENARRATIVE_LLM_MODEL` / `LLM_MODEL` / `OPENAI_MODEL`;`GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 时 Planner 和 Generator 使用流式请求;缺少配置或模型返回非法 JSON 时直接失败,不静默回退固定模板。
|
||||
- `game.generate_draft` 使用 OpenAI-compatible LLM 配置生成结构化 JSON 草案,读取 `GENARRATIVE_GAME_CREATOR_LLM_API_KEY` / `GENARRATIVE_LLM_API_KEY` / `LLM_API_KEY` / `OPENAI_API_KEY`、`GENARRATIVE_GAME_CREATOR_LLM_BASE_URL` / `GENARRATIVE_LLM_BASE_URL` / `LLM_BASE_URL` / `OPENAI_BASE_URL`、`GENARRATIVE_GAME_CREATOR_LLM_MODEL` / `GENARRATIVE_LLM_MODEL` / `LLM_MODEL` / `OPENAI_MODEL`;默认协议为 Responses,可通过 `GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL=chat_completions` 切回旧 Chat Completions 兼容网关;`GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 时 Planner 和 Generator 使用流式请求;缺少配置或模型返回非法 JSON 时直接失败,不静默回退固定模板。
|
||||
- 聊天输入 `/llm-status` 会触发只读 `llm.config_check`,确认 LLM base_url、model 和 API Key 是否已从环境变量读取;状态消息不会显示或保存 API Key。
|
||||
- `game.generate_draft` 的 LLM JSON 必须包含 `handoffs` 数组,覆盖 `design`、`balance`、`art`、`audio`、`code`、`publishing` 6 个专业组;每组必须给出 role、summary、outputs 和 next,缺组或交接内容不完整会判定为模型输出无效并进入返工。
|
||||
- `game.generate_draft` 的真实生成路径使用最小 Planner / Orchestrator / 组内角色 agent / Generator / Evaluator loop:Planner 写 `.agent/spec.md`;每轮 Orchestrator 先写 `.agent/passes/pass-N/agenda.md` 和 `.agent/passes/pass-N/task-graph.json`,首轮全量调度 16 个角色任务,返工轮按 `.agent/findings.md` 生成结构化 `repairRoutes`,重跑命中问题的角色任务及其下游依赖任务,其余角色 brief 从上一轮 carry-over;`task-graph.json` 记录 activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和按依赖排序的 dependencyWaves;角色 brief 写入 `.agent/passes/pass-N/groups/<group>/*.md`,再汇总为 `.agent/passes/pass-N/groups/*.md`;Generator 必须读取用户需求、记忆、`.agent/spec.md`、本轮 `agenda.md`、`task-graph.json`、`.agent/findings.md` 和 6 组汇总 brief 后返回结构化 JSON;每轮会把 Generator 草案拆成 6 组交接快照,写入 `.agent/passes/pass-N/`;Evaluator 做质量评审并写 `.agent/findings.md`,通过后才进入 `game.static_smoke` 静态自检和预览试玩。
|
||||
|
||||
@@ -205,7 +205,7 @@ npm run check:server-rs-ddd
|
||||
|
||||
## 外部服务与资产
|
||||
|
||||
- LLM:通用 LLM 门面继续使用 `GENARRATIVE_LLM_*`;创意 Agent `gpt-5` Responses / Chat Completions 文本链路已于 2026-06 从 APIMart 迁移到 VectorEngine,使用 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` 构造 OpenAI-compatible client,`api-server` 会把未带 `/v1` 的 VectorEngine base URL 规范化到 `/v1` 后请求 `/responses`。`APIMART_BASE_URL` / `APIMART_API_KEY` 只作为历史残留,不再作为创意 Agent gpt-5 客户端来源;后续排障时优先确认 VectorEngine `/v1/models`、`/v1/chat/completions` 和 `/v1/responses` 可用性。
|
||||
- LLM:通用 LLM 门面继续使用 `GENARRATIVE_LLM_*`;`platform-llm` 文本请求默认走 Responses,旧 `/api/llm/chat/completions` 代理和少数旧运行态聊天显式保留 Chat Completions 兼容协议;创意 Agent `gpt-5` Responses / Chat Completions 文本链路已于 2026-06 从 APIMart 迁移到 VectorEngine,使用 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` 构造 OpenAI-compatible client,`api-server` 会把未带 `/v1` 的 VectorEngine base URL 规范化到 `/v1` 后请求 `/responses`。`APIMART_BASE_URL` / `APIMART_API_KEY` 只作为历史残留,不再作为创意 Agent gpt-5 客户端来源;后续排障时优先确认 VectorEngine `/v1/models`、`/v1/chat/completions` 和 `/v1/responses` 可用性。
|
||||
- 图片生成:VectorEngine `gpt-image-2` 图片 provider 归属 `platform-image`,密钥只在后端环境变量中;`api-server` 内的 `openai_image_generation.rs` 只是兼容调用面和外部失败审计桥接,不再承载 provider 协议实现。实际外部生成运行记录统一落 `tracking_event`,`event_key = external_generation_run`,metadata 记录开始 / 结束时间、耗时、状态、成功标记、失败原因、provider task id 和结果摘要,不再写回过时的 `ai_task`。DashScope 只按仍在使用的历史能力单独处理,不作为 GPT-image-2 兜底。VectorEngine `/v1/images/generations` 和 `/v1/images/edits` 上游 POST 使用 `libcurl` 发送;`reqwest` 只保留给参考图 URL 下载和响应中图片 URL 下载。`/v1/images/edits` 的 multipart 参考图必须作为 libcurl 文件上传 part 发送,字段名为 `image`,实现上使用 `Form::buffer(file_name, bytes)` 并设置 `Content-Type`;不能只用 `contents(...).filename(...)`,否则上游会把请求转码为缺少图片并返回 `image is required`。`request_send` 阶段的 curl timeout / connect error 按可重试传输错误处理,最多尝试 5 次,并使用指数退避加短抖动;排障时优先看 `attempt`、`max_attempts`、`retry_delay_ms`、`reference_image_bytes_total` 和 `request_params`,不要把 `SendRequest` 当成上游业务错误。
|
||||
- Match3D 物品 sheet:关卡整图完成后走 VectorEngine `/v1/images/edits` multipart `image`,模型为 `gpt-image-2`,`2K 1:1` 输出 `10*10` spritesheet;物品 sheet prompt 固定要求单一纯绿色 `#00FF00 / RGB(0,255,0)` 绿幕背景,后端上传 OSS 前必须把绿幕扣成透明 PNG,并把透明整图写入 `itemSpritesheetImageSrc/itemSpritesheetImageObjectKey`。后端优先按透明 alpha 连通域从该 sheet 识别真实素材矩形并持久化 20 个物品、每个 5 个形态;识别数量不足时才回退 `10*10` 固定网格。通用系列素材图集的行列索引按每行 2 个物品计算,必须落在 `1..=10`,难度只决定运行态加载 3 / 9 / 15 / 20 种。
|
||||
- Match3D UI spritesheet 和背景派生图:关卡整图作为参考图并发生成 `1K 1:1` UI spritesheet 与 `1K 9:16` 背景图,模型均为 `gpt-image-2`。UI spritesheet prompt 固定要求单一纯绿色 `#00FF00 / RGB(0,255,0)` 绿幕背景,后端上传 OSS 前必须把绿幕扣成透明 PNG;背景图必须合成为全画幅不透明 PNG。
|
||||
|
||||
@@ -455,6 +455,8 @@ OpenTelemetry 现阶段默认开启 OTLP traces / metrics / logs,但本地日
|
||||
|
||||
结构化创作 / RPG 的 Responses JSON 链路默认不打开 `web_search`;本地和生产如需联网增强,必须显式配置 `GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED=true` 或 `GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED=true`。如果上游未开通工具,Responses 可能先吐自然语言再返回 `ToolNotOpen`,这类报错应按工具不可用排查,不要先当成 JSON 解析 bug。
|
||||
|
||||
`platform-llm` 文本请求默认使用 Responses 协议;需要接旧 OpenAI Chat Completions 兼容网关时,调用方必须显式选择 Chat Completions。AI 游戏创作独立 App 也默认使用 Responses,可在本地 `.env.secrets.local` 中设置 `GENARRATIVE_GAME_CREATOR_LLM_PROTOCOL=chat_completions` 兼容旧测试网关。
|
||||
|
||||
创意 Agent `gpt-5` 文本链路已从 APIMart 切到 VectorEngine:`api-server` 读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` 构造 OpenAI-compatible LLM client,并自动补齐 `/v1` 前缀用于 Responses 协议。排查或切换密钥后,可在本地运行:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -189,13 +189,14 @@ mod tests {
|
||||
body: r#"{"id":"resp_api_server_01","model":"ark-router-test","choices":[{"message":{"content":"代理成功"},"finish_reason":"stop"}]}"#.to_string(),
|
||||
extra_headers: Vec::new(),
|
||||
}]);
|
||||
let state = seed_authenticated_state(AppConfig {
|
||||
let (state, user_id) = seed_authenticated_state(AppConfig {
|
||||
llm_base_url: server_url,
|
||||
llm_api_key: Some("test-key".to_string()),
|
||||
llm_model: "ark-router-test".to_string(),
|
||||
..AppConfig::default()
|
||||
})
|
||||
.await;
|
||||
let token = issue_access_token(&state);
|
||||
let token = issue_access_token(&state, user_id.as_str());
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
@@ -264,13 +265,14 @@ mod tests {
|
||||
.to_string(),
|
||||
extra_headers: vec![("x-request-id", "req_llm_stream_01")],
|
||||
}]);
|
||||
let state = seed_authenticated_state(AppConfig {
|
||||
let (state, user_id) = seed_authenticated_state(AppConfig {
|
||||
llm_base_url: server_url,
|
||||
llm_api_key: Some("test-key".to_string()),
|
||||
llm_model: "ark-router-test".to_string(),
|
||||
..AppConfig::default()
|
||||
})
|
||||
.await;
|
||||
let token = issue_access_token(&state);
|
||||
let token = issue_access_token(&state, user_id.as_str());
|
||||
let app = build_router(state);
|
||||
|
||||
let response = app
|
||||
@@ -320,21 +322,21 @@ mod tests {
|
||||
assert!(body_text.contains("data: [DONE]"));
|
||||
}
|
||||
|
||||
async fn seed_authenticated_state(config: AppConfig) -> AppState {
|
||||
async fn seed_authenticated_state(config: AppConfig) -> (AppState, String) {
|
||||
let state = AppState::new(config).expect("state should build");
|
||||
state
|
||||
let user_id = state
|
||||
.seed_test_phone_user_with_password("13800138101", "secret123")
|
||||
.await
|
||||
.id;
|
||||
state
|
||||
(state, user_id)
|
||||
}
|
||||
|
||||
fn issue_access_token(state: &AppState) -> String {
|
||||
fn issue_access_token(state: &AppState, user_id: &str) -> String {
|
||||
let claims = AccessTokenClaims::from_input(
|
||||
AccessTokenClaimsInput {
|
||||
user_id: "user_00000001".to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
session_id: state
|
||||
.seed_test_refresh_session_for_user_id("user_00000001", "sess_llm_proxy"),
|
||||
.seed_test_refresh_session_for_user_id(user_id, "sess_llm_proxy"),
|
||||
provider: AuthProvider::Password,
|
||||
roles: vec!["user".to_string()],
|
||||
token_version: 2,
|
||||
|
||||
@@ -235,7 +235,8 @@ where
|
||||
let mut reply_request = LlmTextRequest::new(vec![
|
||||
LlmMessage::system(NPC_CHAT_TURN_REPLY_SYSTEM_PROMPT),
|
||||
LlmMessage::user(reply_prompt),
|
||||
]);
|
||||
])
|
||||
.with_chat_completions_api();
|
||||
reply_request.max_tokens = Some(700);
|
||||
reply_request.enable_web_search = state.config.rpg_llm_web_search_enabled;
|
||||
reply_request.model = Some(RPG_STORY_LLM_MODEL.to_string());
|
||||
@@ -263,7 +264,8 @@ where
|
||||
let mut suggestion_request = LlmTextRequest::new(vec![
|
||||
LlmMessage::system(NPC_CHAT_TURN_SUGGESTION_SYSTEM_PROMPT),
|
||||
LlmMessage::user(suggestion_prompt),
|
||||
]);
|
||||
])
|
||||
.with_chat_completions_api();
|
||||
suggestion_request.max_tokens = Some(200);
|
||||
suggestion_request.enable_web_search = state.config.rpg_llm_web_search_enabled;
|
||||
suggestion_request.model = Some(RPG_STORY_LLM_MODEL.to_string());
|
||||
|
||||
@@ -585,7 +585,8 @@ async fn request_runtime_plain_text(
|
||||
let mut request = LlmTextRequest::new(vec![
|
||||
LlmMessage::system(system_prompt),
|
||||
LlmMessage::user(user_prompt),
|
||||
]);
|
||||
])
|
||||
.with_chat_completions_api();
|
||||
request.max_tokens = Some(400);
|
||||
request.enable_web_search = state.config.rpg_llm_web_search_enabled;
|
||||
request.model = Some(RPG_STORY_LLM_MODEL.to_string());
|
||||
@@ -617,7 +618,8 @@ fn stream_plain_text_response<'a>(
|
||||
let mut request = LlmTextRequest::new(vec![
|
||||
LlmMessage::system(system_prompt),
|
||||
LlmMessage::user(user_prompt),
|
||||
]);
|
||||
])
|
||||
.with_chat_completions_api();
|
||||
request.max_tokens = Some(700);
|
||||
request.enable_web_search = enable_web_search;
|
||||
request.model = Some(RPG_STORY_LLM_MODEL.to_string());
|
||||
|
||||
@@ -83,7 +83,7 @@ pub struct LlmTextRequest {
|
||||
pub request_timeout_ms: Option<u64>,
|
||||
}
|
||||
|
||||
// 文本协议必须由业务请求显式选择,避免全局默认模型把不同场景混到同一上游形态。
|
||||
// 默认走 Responses;旧 OpenAI Chat Completions 兼容入口显式选择。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum LlmTextProtocol {
|
||||
ChatCompletions,
|
||||
@@ -517,7 +517,7 @@ impl LlmTextRequest {
|
||||
messages,
|
||||
max_tokens: None,
|
||||
enable_web_search: false,
|
||||
protocol: LlmTextProtocol::ChatCompletions,
|
||||
protocol: LlmTextProtocol::Responses,
|
||||
request_timeout_ms: None,
|
||||
}
|
||||
}
|
||||
@@ -534,6 +534,11 @@ impl LlmTextRequest {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_protocol(mut self, protocol: LlmTextProtocol) -> Self {
|
||||
self.protocol = protocol;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
|
||||
self.max_tokens = Some(max_tokens);
|
||||
self
|
||||
@@ -549,6 +554,11 @@ impl LlmTextRequest {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_chat_completions_api(mut self) -> Self {
|
||||
self.protocol = LlmTextProtocol::ChatCompletions;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_request_timeout_ms(mut self, request_timeout_ms: u64) -> Self {
|
||||
self.request_timeout_ms = Some(request_timeout_ms);
|
||||
self
|
||||
@@ -1717,6 +1727,17 @@ mod tests {
|
||||
assert!(config.with_official_fallback(true).official_fallback());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_request_defaults_to_responses_protocol() {
|
||||
let request = LlmTextRequest::single_turn("系统", "用户");
|
||||
|
||||
assert_eq!(request.protocol, LlmTextProtocol::Responses);
|
||||
assert_eq!(
|
||||
request.with_chat_completions_api().protocol,
|
||||
LlmTextProtocol::ChatCompletions
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_text_sends_official_fallback_for_openai_compatible_clients() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind");
|
||||
@@ -1817,7 +1838,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_text_parses_non_stream_response() {
|
||||
async fn request_text_parses_chat_completions_non_stream_response() {
|
||||
let server_url = spawn_mock_server(vec![MockResponse {
|
||||
status_line: "200 OK",
|
||||
content_type: "application/json; charset=utf-8",
|
||||
@@ -1827,7 +1848,7 @@ mod tests {
|
||||
|
||||
let client = build_test_client(server_url, 0);
|
||||
let response = client
|
||||
.request_single_message_text("系统", "用户")
|
||||
.request_text(LlmTextRequest::single_turn("系统", "用户").with_chat_completions_api())
|
||||
.await
|
||||
.expect("request_text should succeed");
|
||||
|
||||
@@ -1865,7 +1886,7 @@ mod tests {
|
||||
|
||||
let client = build_test_client(server_url, 1);
|
||||
let response = client
|
||||
.request_single_message_text("系统", "用户")
|
||||
.request_text(LlmTextRequest::single_turn("系统", "用户").with_chat_completions_api())
|
||||
.await
|
||||
.expect("second attempt should succeed");
|
||||
|
||||
@@ -1907,7 +1928,11 @@ mod tests {
|
||||
let client = LlmClient::new(config).expect("client should be created");
|
||||
|
||||
let error = client
|
||||
.request_text(LlmTextRequest::single_turn("系统", "用户").with_request_timeout_ms(20))
|
||||
.request_text(
|
||||
LlmTextRequest::single_turn("系统", "用户")
|
||||
.with_chat_completions_api()
|
||||
.with_request_timeout_ms(20),
|
||||
)
|
||||
.await
|
||||
.expect_err("request override should timeout before the global timeout");
|
||||
|
||||
@@ -1937,6 +1962,7 @@ mod tests {
|
||||
let response = client
|
||||
.request_text(
|
||||
LlmTextRequest::single_turn("系统", "用户")
|
||||
.with_chat_completions_api()
|
||||
.with_web_search(true)
|
||||
.with_max_tokens(128),
|
||||
)
|
||||
@@ -1988,17 +2014,20 @@ mod tests {
|
||||
.with_official_fallback(true);
|
||||
let client = LlmClient::new(config).expect("client should be created");
|
||||
let response = client
|
||||
.request_text(LlmTextRequest::new(vec![
|
||||
LlmMessage::system("你是拼图关卡命名编辑"),
|
||||
LlmMessage::user_multimodal(vec![
|
||||
LlmMessageContentPart::InputText {
|
||||
text: "画面描述:一只猫在雨夜灯牌下回头。".to_string(),
|
||||
},
|
||||
LlmMessageContentPart::InputImage {
|
||||
image_url: "data:image/png;base64,abcd".to_string(),
|
||||
},
|
||||
]),
|
||||
]))
|
||||
.request_text(
|
||||
LlmTextRequest::new(vec![
|
||||
LlmMessage::system("你是拼图关卡命名编辑"),
|
||||
LlmMessage::user_multimodal(vec![
|
||||
LlmMessageContentPart::InputText {
|
||||
text: "画面描述:一只猫在雨夜灯牌下回头。".to_string(),
|
||||
},
|
||||
LlmMessageContentPart::InputImage {
|
||||
image_url: "data:image/png;base64,abcd".to_string(),
|
||||
},
|
||||
]),
|
||||
])
|
||||
.with_chat_completions_api(),
|
||||
)
|
||||
.await
|
||||
.expect("request_text should succeed");
|
||||
|
||||
@@ -2168,9 +2197,12 @@ mod tests {
|
||||
let client = build_test_client(server_url, 0);
|
||||
let mut updates = Vec::new();
|
||||
let response = client
|
||||
.stream_single_message_text("系统", "用户", |delta| {
|
||||
updates.push(delta.accumulated_text.clone());
|
||||
})
|
||||
.stream_text(
|
||||
LlmTextRequest::single_turn("系统", "用户").with_chat_completions_api(),
|
||||
|delta| {
|
||||
updates.push(delta.accumulated_text.clone());
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("stream_text should succeed");
|
||||
|
||||
@@ -2234,7 +2266,9 @@ mod tests {
|
||||
|
||||
let client = build_test_client(server_url, 0);
|
||||
let error = client
|
||||
.request_single_message_text("系统原文", "用户原文")
|
||||
.request_text(
|
||||
LlmTextRequest::single_turn("系统原文", "用户原文").with_chat_completions_api(),
|
||||
)
|
||||
.await
|
||||
.expect_err("invalid json should fail");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user