合并远端创作应用分支
合并 origin/codex/ai-game-creator-app 的最新提交。 保留本地美术资源生成接口与素材库合并改动。
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
"apiKey": "",
|
||||
"baseUrl": "https://api.openai.com/v1",
|
||||
"model": "gpt-4.1",
|
||||
"protocol": "responses",
|
||||
"apiKind": "openai_responses",
|
||||
"stream": false,
|
||||
"requestTimeoutMs": 180000,
|
||||
"maxRetries": 0,
|
||||
|
||||
@@ -357,7 +357,7 @@ for (const snippet of [
|
||||
'requestJson?.stream === true',
|
||||
`requestBodies.every((body) => body.includes('"stream":true'))`,
|
||||
"const localConfigPath = path.join(appRoot, 'game-creator.config.local.json')",
|
||||
"protocol: 'chat_completions'",
|
||||
"apiKind: 'openai_chat'",
|
||||
'stream: true',
|
||||
'await restoreOptionalFile(localConfigPath, previousLocalConfig)',
|
||||
"method: 'HEAD'",
|
||||
|
||||
@@ -577,7 +577,7 @@ async function writeSmokeLocalConfig(baseUrl) {
|
||||
apiKey: 'local-provider-key',
|
||||
baseUrl,
|
||||
model: 'local-game-creator-smoke',
|
||||
protocol: 'chat_completions',
|
||||
apiKind: 'openai_chat',
|
||||
stream: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -92,6 +92,8 @@ const child = spawn(
|
||||
{
|
||||
cwd: appRoot,
|
||||
stdio: 'inherit',
|
||||
// Node 18.20+/20+/24 on Windows rejects spawning .cmd (npm.cmd) without a shell (EINVAL).
|
||||
shell: true,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use platform_agent::{
|
||||
route_game_creation_repair_issues,
|
||||
};
|
||||
use platform_llm::{
|
||||
LlmClient, LlmConfig, LlmMessage, LlmProvider, LlmTextProtocol, LlmTextRequest,
|
||||
LlmClient, LlmConfig, LlmMessage, LlmProvider, LlmApiKind, LlmRunRequest,
|
||||
DEFAULT_RETRY_BACKOFF_MS,
|
||||
};
|
||||
use reqwest::header;
|
||||
@@ -92,7 +92,7 @@ struct GameCreatorLlmConfigStatus {
|
||||
api_key_present: bool,
|
||||
base_url: Option<String>,
|
||||
model: Option<String>,
|
||||
protocol: String,
|
||||
api_kind: String,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ struct GameCreatorLlmConfigFile {
|
||||
api_key: Option<String>,
|
||||
base_url: Option<String>,
|
||||
model: Option<String>,
|
||||
protocol: Option<String>,
|
||||
api_kind: Option<String>,
|
||||
stream: Option<bool>,
|
||||
request_timeout_ms: Option<u64>,
|
||||
max_retries: Option<u32>,
|
||||
@@ -136,7 +136,7 @@ struct GameCreatorLlmConfig {
|
||||
api_key: String,
|
||||
base_url: String,
|
||||
model: String,
|
||||
protocol: String,
|
||||
api_kind: String,
|
||||
stream: bool,
|
||||
request_timeout_ms: u64,
|
||||
max_retries: u32,
|
||||
@@ -446,7 +446,7 @@ const GAME_CREATOR_CONFIG_FILE_NAME: &str = "game-creator.config.json";
|
||||
const GAME_CREATOR_LOCAL_CONFIG_FILE_NAME: &str = "game-creator.config.local.json";
|
||||
const DEFAULT_GAME_CREATOR_LLM_BASE_URL: &str = "https://api.openai.com/v1";
|
||||
const DEFAULT_GAME_CREATOR_LLM_MODEL: &str = "gpt-4.1";
|
||||
const DEFAULT_GAME_CREATOR_LLM_PROTOCOL: &str = "responses";
|
||||
const DEFAULT_GAME_CREATOR_LLM_API_KIND: &str = "openai_responses";
|
||||
const DEFAULT_CANVAS_SYNC_API_BASE_URL: &str = "http://127.0.0.1:8082";
|
||||
const DEFAULT_GAME_CREATOR_APP_CONFIG_JSON: &str = include_str!("../../game-creator.config.json");
|
||||
const GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS: u32 = 320000;
|
||||
@@ -490,7 +490,7 @@ impl Default for GameCreatorLlmConfig {
|
||||
api_key: String::new(),
|
||||
base_url: DEFAULT_GAME_CREATOR_LLM_BASE_URL.to_string(),
|
||||
model: DEFAULT_GAME_CREATOR_LLM_MODEL.to_string(),
|
||||
protocol: DEFAULT_GAME_CREATOR_LLM_PROTOCOL.to_string(),
|
||||
api_kind: DEFAULT_GAME_CREATOR_LLM_API_KIND.to_string(),
|
||||
stream: false,
|
||||
request_timeout_ms: GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS,
|
||||
max_retries: 0,
|
||||
@@ -1441,23 +1441,24 @@ fn build_game_creator_llm_client_from_config() -> Result<LlmClient, String> {
|
||||
LlmClient::new(config).map_err(|error| format!("LLM client 初始化失败:{error}"))
|
||||
}
|
||||
|
||||
fn read_game_creator_llm_protocol_from_config() -> Result<LlmTextProtocol, String> {
|
||||
fn read_game_creator_llm_api_kind_from_config() -> Result<LlmApiKind, String> {
|
||||
let app_config = load_game_creator_app_config()?;
|
||||
parse_game_creator_llm_protocol(&app_config.llm.protocol)
|
||||
parse_game_creator_llm_api_kind(&app_config.llm.api_kind)
|
||||
}
|
||||
|
||||
fn parse_game_creator_llm_protocol(value: &str) -> Result<LlmTextProtocol, String> {
|
||||
fn parse_game_creator_llm_api_kind(value: &str) -> Result<LlmApiKind, String> {
|
||||
let normalized = value.trim().to_ascii_lowercase().replace('-', "_");
|
||||
let normalized = if normalized.is_empty() {
|
||||
DEFAULT_GAME_CREATOR_LLM_PROTOCOL
|
||||
DEFAULT_GAME_CREATOR_LLM_API_KIND
|
||||
} else {
|
||||
normalized.as_str()
|
||||
};
|
||||
match normalized {
|
||||
"responses" | "response" | "responses_api" => Ok(LlmTextProtocol::Responses),
|
||||
"chat_completions" | "chat_completion" | "chat" => Ok(LlmTextProtocol::ChatCompletions),
|
||||
"openai_responses" => Ok(LlmApiKind::OpenAiResponses),
|
||||
"openai_chat" => Ok(LlmApiKind::OpenAiChat),
|
||||
"anthropic" => Ok(LlmApiKind::Anthropic),
|
||||
value => Err(format!(
|
||||
"LLM protocol 无效:{value},请使用 responses 或 chat_completions"
|
||||
"LLM api_kind 无效:{value},请使用 openai_responses、openai_chat 或 anthropic"
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -1471,18 +1472,18 @@ fn check_game_creator_llm_config_from_config() -> GameCreatorLlmConfigStatus {
|
||||
api_key_present: false,
|
||||
base_url: None,
|
||||
model: None,
|
||||
protocol: DEFAULT_GAME_CREATOR_LLM_PROTOCOL.to_string(),
|
||||
api_kind: DEFAULT_GAME_CREATOR_LLM_API_KIND.to_string(),
|
||||
error: Some(error),
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut status = check_game_creator_llm_config_values(&app_config.llm);
|
||||
status.protocol = parse_game_creator_llm_protocol(&app_config.llm.protocol)
|
||||
.map(game_creator_llm_protocol_name)
|
||||
status.api_kind = parse_game_creator_llm_api_kind(&app_config.llm.api_kind)
|
||||
.map(game_creator_llm_api_kind_name)
|
||||
.unwrap_or_else(|error| {
|
||||
status.configured = false;
|
||||
status.error = Some(error);
|
||||
DEFAULT_GAME_CREATOR_LLM_PROTOCOL.to_string()
|
||||
DEFAULT_GAME_CREATOR_LLM_API_KIND.to_string()
|
||||
});
|
||||
if status.configured {
|
||||
if let Err(error) = build_game_creator_llm_client_from_config() {
|
||||
@@ -1525,15 +1526,16 @@ fn check_game_creator_llm_config_values(
|
||||
api_key_present,
|
||||
base_url,
|
||||
model,
|
||||
protocol: DEFAULT_GAME_CREATOR_LLM_PROTOCOL.to_string(),
|
||||
api_kind: DEFAULT_GAME_CREATOR_LLM_API_KIND.to_string(),
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
fn game_creator_llm_protocol_name(protocol: LlmTextProtocol) -> String {
|
||||
match protocol {
|
||||
LlmTextProtocol::ChatCompletions => "chat_completions",
|
||||
LlmTextProtocol::Responses => "responses",
|
||||
fn game_creator_llm_api_kind_name(api_kind: LlmApiKind) -> String {
|
||||
match api_kind {
|
||||
LlmApiKind::OpenAiChat => "openai_chat",
|
||||
LlmApiKind::OpenAiResponses => "openai_responses",
|
||||
LlmApiKind::Anthropic => "anthropic",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
@@ -1916,7 +1918,7 @@ async fn request_planner_spec_with_client(
|
||||
short_memory: &str,
|
||||
long_memory: &str,
|
||||
) -> Result<String, String> {
|
||||
let request = LlmTextRequest::new(vec![
|
||||
let request = LlmRunRequest::new(vec![
|
||||
LlmMessage::system(game_creator_planner_system_prompt()),
|
||||
LlmMessage::user(game_creator_planner_user_prompt(
|
||||
prompt,
|
||||
@@ -1924,12 +1926,12 @@ async fn request_planner_spec_with_client(
|
||||
long_memory,
|
||||
)),
|
||||
])
|
||||
.with_protocol(read_game_creator_llm_protocol_from_config()?)
|
||||
.with_max_tokens(GAME_CREATOR_PLANNER_MAX_OUTPUT_TOKENS);
|
||||
.with_api_kind(read_game_creator_llm_api_kind_from_config()?)
|
||||
.with_max_output_tokens(GAME_CREATOR_PLANNER_MAX_OUTPUT_TOKENS);
|
||||
let response = request_game_creator_llm_text(client, request)
|
||||
.await
|
||||
.map_err(|error| format!("Planner 生成失败:{error}"))?;
|
||||
let spec = strip_llm_thinking_blocks(response.content.as_str());
|
||||
let spec = strip_llm_thinking_blocks(response.text.as_str());
|
||||
if spec.is_empty() {
|
||||
Err("Planner 未返回规格".to_string())
|
||||
} else {
|
||||
@@ -1964,12 +1966,12 @@ async fn request_generator_game_draft_with_client(
|
||||
const MAX_EMPTY_RETRIES: u32 = 3;
|
||||
let mut empty_retries = 0u32;
|
||||
let response = loop {
|
||||
let request = LlmTextRequest::new(vec![
|
||||
let request = LlmRunRequest::new(vec![
|
||||
LlmMessage::system(system_prompt),
|
||||
LlmMessage::user(user_prompt.clone()),
|
||||
])
|
||||
.with_protocol(read_game_creator_llm_protocol_from_config()?)
|
||||
.with_max_tokens(GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS);
|
||||
.with_api_kind(read_game_creator_llm_api_kind_from_config()?)
|
||||
.with_max_output_tokens(GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS);
|
||||
match request_game_creator_llm_text(client, request).await {
|
||||
Ok(response) => break response,
|
||||
Err(platform_llm::LlmError::EmptyResponse) if empty_retries < MAX_EMPTY_RETRIES => {
|
||||
@@ -1997,19 +1999,19 @@ async fn request_generator_game_draft_with_client(
|
||||
};
|
||||
// 先把原始返回落盘,再解析;解析失败(如输出截断)时仍能从仓库里拿到完整原文(仅 debug 构建)。
|
||||
#[cfg(all(debug_assertions, not(test)))]
|
||||
debug::persist_snapshot(response.content.as_str());
|
||||
let content = strip_llm_thinking_blocks(response.content.as_str());
|
||||
debug::persist_snapshot(response.text.as_str());
|
||||
let content = strip_llm_thinking_blocks(response.text.as_str());
|
||||
parse_llm_game_draft_response(content.as_str())
|
||||
}
|
||||
|
||||
async fn request_game_creator_llm_text(
|
||||
client: &LlmClient,
|
||||
request: LlmTextRequest,
|
||||
) -> Result<platform_llm::LlmTextResponse, platform_llm::LlmError> {
|
||||
request: LlmRunRequest,
|
||||
) -> Result<platform_llm::LlmRunResponse, platform_llm::LlmError> {
|
||||
if game_creator_llm_stream_enabled() {
|
||||
client.stream_text(request, |_| {}).await
|
||||
client.stream_run(request, |_| {}).await
|
||||
} else {
|
||||
client.request_text(request).await
|
||||
client.run(request).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4863,8 +4865,8 @@ fn merge_game_creator_llm_config(
|
||||
if let Some(value) = patch.model {
|
||||
config.model = value;
|
||||
}
|
||||
if let Some(value) = patch.protocol {
|
||||
config.protocol = value;
|
||||
if let Some(value) = patch.api_kind {
|
||||
config.api_kind = value;
|
||||
}
|
||||
if let Some(value) = patch.stream {
|
||||
config.stream = value;
|
||||
@@ -4908,8 +4910,8 @@ fn normalize_game_creator_app_config(
|
||||
config.llm.base_url =
|
||||
trim_config_string(&config.llm.base_url).ok_or_else(llm_base_url_config_error)?;
|
||||
config.llm.model = trim_config_string(&config.llm.model).ok_or_else(llm_model_config_error)?;
|
||||
config.llm.protocol = game_creator_llm_protocol_name(parse_game_creator_llm_protocol(
|
||||
&config.llm.protocol,
|
||||
config.llm.api_kind = game_creator_llm_api_kind_name(parse_game_creator_llm_api_kind(
|
||||
&config.llm.api_kind,
|
||||
)?);
|
||||
if config.llm.request_timeout_ms == 0 {
|
||||
return Err("配置项 llm.requestTimeoutMs 必须大于 0".to_string());
|
||||
@@ -6591,7 +6593,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);
|
||||
println!("llm.apiKind={}", status.api_kind);
|
||||
if let Some(error) = status.error {
|
||||
println!("llm.error={error}");
|
||||
}
|
||||
@@ -6823,7 +6825,7 @@ mod tests {
|
||||
"apiKey": "file-key",
|
||||
"baseUrl": "https://example.test/v1",
|
||||
"model": "model-from-file",
|
||||
"protocol": "chat_completions",
|
||||
"apiKind": "openai_chat",
|
||||
"stream": true,
|
||||
"requestTimeoutMs": 42000,
|
||||
"maxRetries": 2,
|
||||
@@ -6844,7 +6846,7 @@ mod tests {
|
||||
assert_eq!(config.llm.api_key, "file-key");
|
||||
assert_eq!(config.llm.base_url, "https://example.test/v1");
|
||||
assert_eq!(config.llm.model, "model-from-file");
|
||||
assert_eq!(config.llm.protocol, "chat_completions");
|
||||
assert_eq!(config.llm.api_kind, "openai_chat");
|
||||
assert!(config.llm.stream);
|
||||
assert_eq!(config.llm.request_timeout_ms, 42_000);
|
||||
assert_eq!(config.llm.max_retries, 2);
|
||||
@@ -6893,7 +6895,7 @@ mod tests {
|
||||
api_key: " unit-test-key ".to_string(),
|
||||
base_url: " https://runtime.example.test/v1 ".to_string(),
|
||||
model: " runtime-model ".to_string(),
|
||||
protocol: "chat".to_string(),
|
||||
api_kind: "openai_chat".to_string(),
|
||||
stream: true,
|
||||
request_timeout_ms: 42_000,
|
||||
max_retries: 2,
|
||||
@@ -6914,7 +6916,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(saved.config.llm.api_key, "unit-test-key");
|
||||
assert_eq!(saved.config.llm.base_url, "https://runtime.example.test/v1");
|
||||
assert_eq!(saved.config.llm.protocol, "chat_completions");
|
||||
assert_eq!(saved.config.llm.api_kind, "openai_chat");
|
||||
assert_eq!(saved.config.editor_api.api_key, "editor-key");
|
||||
assert!(root.join(GAME_CREATOR_CONFIG_FILE_NAME).is_file());
|
||||
|
||||
@@ -6925,7 +6927,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_config_write_rejects_invalid_protocol() {
|
||||
fn app_config_write_rejects_invalid_api_kind() {
|
||||
let root = unique_project_path();
|
||||
fs::create_dir_all(&root).expect("runtime config dir");
|
||||
let _guard = use_test_runtime_config_dir(root.clone());
|
||||
@@ -6933,15 +6935,15 @@ mod tests {
|
||||
let result = write_game_creator_app_config(GameCreatorAppConfig {
|
||||
llm: GameCreatorLlmConfig {
|
||||
api_key: String::new(),
|
||||
protocol: "legacy".to_string(),
|
||||
api_kind: "legacy".to_string(),
|
||||
..GameCreatorLlmConfig::default()
|
||||
},
|
||||
editor_api: GameCreatorEditorApiConfig::default(),
|
||||
});
|
||||
|
||||
assert!(result
|
||||
.expect_err("invalid protocol")
|
||||
.contains("LLM protocol 无效"));
|
||||
.expect_err("invalid api_kind")
|
||||
.contains("LLM api_kind 无效"));
|
||||
assert!(!root.join(GAME_CREATOR_CONFIG_FILE_NAME).exists());
|
||||
fs::remove_dir_all(root).expect("cleanup runtime config dir");
|
||||
}
|
||||
@@ -7384,7 +7386,7 @@ mod tests {
|
||||
"apiKey": "test-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "mock-game-model",
|
||||
"protocol": "responses"
|
||||
"apiKind": "openai_responses"
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
@@ -7425,12 +7427,33 @@ mod tests {
|
||||
Some("http://127.0.0.1:1/v1")
|
||||
);
|
||||
assert_eq!(configured.model.as_deref(), Some("mock-game-model"));
|
||||
assert_eq!(configured.protocol, "responses");
|
||||
assert_eq!(configured.api_kind, "openai_responses");
|
||||
assert!(!serde_json::to_string(&configured)
|
||||
.unwrap()
|
||||
.contains("unit-test-api-key"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn llm_api_kind_parses_canonical_names() {
|
||||
assert_eq!(
|
||||
parse_game_creator_llm_api_kind("anthropic"),
|
||||
Ok(LlmApiKind::Anthropic)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_game_creator_llm_api_kind("openai_chat"),
|
||||
Ok(LlmApiKind::OpenAiChat)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_game_creator_llm_api_kind("openai_responses"),
|
||||
Ok(LlmApiKind::OpenAiResponses)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_game_creator_llm_api_kind(""),
|
||||
Ok(LlmApiKind::OpenAiResponses)
|
||||
);
|
||||
assert!(parse_game_creator_llm_api_kind("legacy").is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn agent_loop_writes_spec_findings_and_retries_generator() {
|
||||
let root = unique_project_path();
|
||||
@@ -7904,7 +7927,7 @@ mod tests {
|
||||
"apiKey": "test-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "mock-game-model",
|
||||
"protocol": "responses"
|
||||
"apiKind": "openai_responses"
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
|
||||
@@ -60,18 +60,18 @@ interface GameCreatorLlmConfigStatus {
|
||||
apiKeyPresent: boolean;
|
||||
baseUrl: string | null;
|
||||
model: string | null;
|
||||
protocol: string;
|
||||
apiKind: string;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
type GameCreatorLlmProtocol = 'responses' | 'chat_completions';
|
||||
type GameCreatorLlmApiKind = 'openai_responses' | 'openai_chat' | 'anthropic';
|
||||
|
||||
interface GameCreatorAppConfig {
|
||||
llm: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
model: string;
|
||||
protocol: GameCreatorLlmProtocol;
|
||||
apiKind: GameCreatorLlmApiKind;
|
||||
stream: boolean;
|
||||
requestTimeoutMs: number;
|
||||
maxRetries: number;
|
||||
@@ -326,7 +326,7 @@ const defaultRuntimeConfigDraft: GameCreatorAppConfig = {
|
||||
apiKey: '',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: 'gpt-4.1',
|
||||
protocol: 'responses',
|
||||
apiKind: 'openai_responses',
|
||||
stream: false,
|
||||
requestTimeoutMs: 180000,
|
||||
maxRetries: 0,
|
||||
@@ -1739,7 +1739,7 @@ export function App() {
|
||||
text: status.configured
|
||||
? `LLM 已配置:${status.model ?? '未命名模型'} @ ${
|
||||
status.baseUrl ?? '未设置 base_url'
|
||||
},${status.protocol},API Key 已读取。`
|
||||
},${status.apiKind},API Key 已读取。`
|
||||
: `LLM 未就绪:${status.error ?? '配置不完整'}。API Key:${
|
||||
status.apiKeyPresent ? '已读取' : '未读取'
|
||||
}。`,
|
||||
@@ -3753,19 +3753,20 @@ export function App() {
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
LLM 协议
|
||||
LLM API 类型
|
||||
<select
|
||||
aria-label="LLM 协议"
|
||||
value={runtimeConfigDraft.llm.protocol}
|
||||
aria-label="LLM API 类型"
|
||||
value={runtimeConfigDraft.llm.apiKind}
|
||||
onChange={(event) =>
|
||||
updateRuntimeLlmConfig(
|
||||
'protocol',
|
||||
event.currentTarget.value as GameCreatorLlmProtocol,
|
||||
'apiKind',
|
||||
event.currentTarget.value as GameCreatorLlmApiKind,
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="responses">responses</option>
|
||||
<option value="chat_completions">chat_completions</option>
|
||||
<option value="openai_responses">openai_responses</option>
|
||||
<option value="openai_chat">openai_chat</option>
|
||||
<option value="anthropic">anthropic</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="settings-checkbox">
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
apiKey: 'unit-loaded-secret-value',
|
||||
baseUrl: 'https://llm.example.test/v1',
|
||||
model: 'gpt-test',
|
||||
protocol: 'responses',
|
||||
apiKind: 'openai_responses',
|
||||
stream: false,
|
||||
requestTimeoutMs: 180000,
|
||||
maxRetries: 0,
|
||||
@@ -102,8 +102,8 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
fireEvent.change(screen.getByLabelText('LLM 模型'), {
|
||||
target: { value: 'gpt-next' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('LLM 协议'), {
|
||||
target: { value: 'chat_completions' },
|
||||
fireEvent.change(screen.getByLabelText('LLM API 类型'), {
|
||||
target: { value: 'openai_chat' },
|
||||
});
|
||||
fireEvent.click(screen.getByLabelText('LLM 流式请求'));
|
||||
fireEvent.change(screen.getByLabelText('LLM 超时 ms'), {
|
||||
@@ -131,7 +131,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
apiKey: 'unit-new-secret-value',
|
||||
baseUrl: 'https://new-llm.example.test/v1',
|
||||
model: 'gpt-next',
|
||||
protocol: 'chat_completions',
|
||||
apiKind: 'openai_chat',
|
||||
stream: true,
|
||||
requestTimeoutMs: 90000,
|
||||
maxRetries: 3,
|
||||
@@ -1484,7 +1484,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
apiKeyPresent: true,
|
||||
baseUrl: 'https://llm.example.test/v1',
|
||||
model: 'gpt-test',
|
||||
protocol: 'responses',
|
||||
apiKind: 'openai_responses',
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
@@ -1497,7 +1497,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'LLM 已配置:gpt-test @ https://llm.example.test/v1,responses,API Key 已读取。',
|
||||
'LLM 已配置:gpt-test @ https://llm.example.test/v1,openai_responses,API Key 已读取。',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(screen.queryByText(/sk-test-secret/)).toBeNull();
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
- 验证方式:运行 `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 追加,2026-06-30 更新:`platform-llm` 的 `LlmTextRequest::new` 默认协议改为 Responses;旧 `/api/llm/chat/completions` 代理、RPG runtime chat 和需要旧测试网关的 AI 游戏创作 smoke 必须显式选择 Chat Completions。AI 游戏创作真实 LLM 默认 Responses,可用客户端配置项 `llm.protocol=chat_completions` 兼容旧 OpenAI Chat Completions 网关。
|
||||
2026-06-27 追加,2026-06-30 更新:`platform-llm` 旧 `LlmTextRequest` / `LlmTextResponse` 已直接替换为 provider-neutral 的 `LlmRunRequest` / `LlmRunResponse`,API kind 先固定为 `openai_chat`、`openai_responses`、`anthropic` 三类。AI 游戏创作 App 改用客户端运行时配置(Tauri 应用配置目录的 `game-creator.config.json`),LLM 维度由 `llm.apiKind` 控制,默认 `openai_responses`,可设为 `openai_chat` 接旧 Chat Completions 兼容网关,或 `anthropic` 接 Anthropic Messages。当前 run 响应只保留通用文本、finish reason、response id 和 usage,高级能力后续按 capability 扩展,不把业务层绑死到 Responses 字段。
|
||||
|
||||
## 2026-06-24 AI 游戏创作 App 生成编排使用文件驱动 loop
|
||||
|
||||
|
||||
@@ -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;用于本机联调前确认配置。发布版启动时会在 Tauri 应用配置目录生成默认 `game-creator.config.json`,仓库内 `apps/ai-game-creator-shell/game-creator.config.json` 只作为默认模板。
|
||||
- `npm run ai-game-creator-shell:agent-run -- --no-wait /绝对项目路径 "游戏创作需求"`:使用真实 OpenAI-compatible 配置跑一次本地生成、落盘、自检和预览;用于人工验收真实 provider 路径。真实 provider 配置放在 Tauri 应用配置目录的 `game-creator.config.json` 中,至少设置 `llm.apiKey`,需要覆盖默认服务时设置 `llm.baseUrl`、`llm.model`;默认按 Responses 协议请求,旧 Chat Completions 兼容网关设置 `llm.protocol` 为 `chat_completions`;真实网关长请求若在非流式响应前被 60 秒空闲连接切断,联调时设置 `llm.stream` 为 `true`。
|
||||
- `npm run ai-game-creator-shell:agent-run -- --no-wait /绝对项目路径 "游戏创作需求"`:使用真实 OpenAI-compatible 配置跑一次本地生成、落盘、自检和预览;用于人工验收真实 provider 路径。真实 provider 配置放在 Tauri 应用配置目录的 `game-creator.config.json` 中,至少设置 `llm.apiKey`,需要覆盖默认服务时设置 `llm.baseUrl`、`llm.model`;默认 API kind 为 `openai_responses`,旧 Chat Completions 兼容网关设置 `llm.apiKind` 为 `openai_chat`,Anthropic Messages 网关设置 `llm.apiKind` 为 `anthropic`,URL 会在 base URL 后拼 `/v1/messages`,例如 Minimax Anthropic base URL 可配置为 `https://api.minimaxi.com/anthropic`;真实网关长请求若在非流式响应前被 60 秒空闲连接切断,联调时设置 `llm.stream` 为 `true`。
|
||||
|
||||
## 当前最小落地
|
||||
|
||||
@@ -104,13 +104,13 @@ game-project/
|
||||
- 普通用户侧的生成、上传、运行、自检、预览状态 / 启动 / 打开 / 停止、记忆写入和画板资产导入都必须先完成 `/project` 初始化;未初始化时只提示设置本地项目,不落到默认临时目录。
|
||||
- 终端可用 `npm run ai-game-creator-shell:llm-status` 检查 LLM 客户端配置是否就绪;桌面 App 主窗口“配置”面板可读写 Tauri 应用配置目录中的 `game-creator.config.json`,`/llm-status` / 生成入口读取同一份配置,CLI 开发入口无 AppHandle 时才回退读取仓库旁边的配置模板和 gitignored 本机覆盖文件;不请求上游、不显示 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 预览;发布 App 读取 Tauri 应用配置目录中的 `game-creator.config.json`,开发 CLI 无 AppHandle 时才读取仓库旁边的配置模板和 gitignored 本机覆盖文件,不把 API Key 写入仓库或项目文件。自动验证可加 `--no-wait`,例如 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-test "像素风反弹弹幕厨房"`,生成预览 trace 后立即停止本地预览,避免终端卡在回车等待。默认协议为 Responses;旧 Chat Completions 兼容网关设置 `llm.protocol` 为 `chat_completions`。真实 OpenAI-compatible 网关建议设置 `llm.stream` 为 `true` 跑 Planner 和 Generator,避免长请求非流式空闲断连。
|
||||
- 终端可用 `npm run ai-game-creator-shell:agent-run -- /绝对项目路径 "游戏创作需求"` 跑一次真实 LLM 生成、落盘、`game.static_smoke` 和本地 HTTP 预览;发布 App 读取 Tauri 应用配置目录中的 `game-creator.config.json`,开发 CLI 无 AppHandle 时才读取仓库旁边的配置模板和 gitignored 本机覆盖文件,不把 API Key 写入仓库或项目文件。自动验证可加 `--no-wait`,例如 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-test "像素风反弹弹幕厨房"`,生成预览 trace 后立即停止本地预览,避免终端卡在回车等待。默认 API kind 为 `openai_responses`;旧 Chat Completions 兼容网关设置 `llm.apiKind` 为 `openai_chat`,Anthropic Messages 网关设置 `llm.apiKind` 为 `anthropic`。真实 OpenAI-compatible 网关建议设置 `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 草案,发布 App 的配置项来自 Tauri 应用配置目录中的 `game-creator.config.json`:`llm.apiKey`、`llm.baseUrl`、`llm.model`、`llm.protocol`、`llm.stream`、`llm.requestTimeoutMs`、`llm.maxRetries`、`llm.retryBackoffMs`;默认协议为 Responses,可通过 `llm.protocol=chat_completions` 切回旧 Chat Completions 兼容网关;`llm.stream=true` 时 Planner 和 Generator 使用流式请求;缺少配置或模型返回非法 JSON 时直接失败,不静默回退固定模板。
|
||||
- 主窗口“配置”面板读写 Tauri 应用配置目录中的 `game-creator.config.json`,覆盖 LLM API Key、base URL、模型、协议、流式请求、超时、重试和画板 External API 配置;保存时只写运行时配置文件,不写仓库模板、本地项目、trace 或 manifest。
|
||||
- `game.generate_draft` 使用 OpenAI-compatible LLM 配置生成结构化 JSON 草案,发布 App 的配置项来自 Tauri 应用配置目录中的 `game-creator.config.json`:`llm.apiKey`、`llm.baseUrl`、`llm.model`、`llm.apiKind`、`llm.stream`、`llm.requestTimeoutMs`、`llm.maxRetries`、`llm.retryBackoffMs`;默认 API kind 为 `openai_responses`,可设 `llm.apiKind=openai_chat` 切回旧 Chat Completions 兼容网关,或 `llm.apiKind=anthropic` 走 Anthropic Messages;`llm.stream=true` 时 Planner 和 Generator 使用流式请求;缺少配置或模型返回非法 JSON 时直接失败,不静默回退固定模板。
|
||||
- 主窗口“配置”面板读写 Tauri 应用配置目录中的 `game-creator.config.json`,覆盖 LLM API Key、base URL、模型、API 类型、流式请求、超时、重试和画板 External API 配置;保存时只写运行时配置文件,不写仓库模板、本地项目、trace 或 manifest。
|
||||
- 聊天输入 `/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` 静态自检和预览试玩。
|
||||
|
||||
@@ -455,7 +455,7 @@ 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 是客户端,不读取 `.env`;发布 App 启动时会在 Tauri 应用配置目录生成 `game-creator.config.json`,主窗口“配置”面板读写该运行时文件,真实密钥和本机覆盖项写入该文件,仓库内 `apps/ai-game-creator-shell/game-creator.config.json` 只作为默认模板,开发 CLI 无 AppHandle 时才回退读取仓库旁边的 gitignored 覆盖文件。可用 `llm.protocol=chat_completions` 兼容旧测试网关。
|
||||
`platform-llm` 文本请求默认使用 Responses 协议;需要接旧 OpenAI Chat Completions 兼容网关时,调用方必须显式选择 Chat Completions。AI 游戏创作独立 App 是客户端,不读取 `.env`;发布 App 启动时会在 Tauri 应用配置目录生成 `game-creator.config.json`,主窗口“配置”面板读写该运行时文件,真实密钥和本机覆盖项写入该文件,仓库内 `apps/ai-game-creator-shell/game-creator.config.json` 只作为默认模板,开发 CLI 无 AppHandle 时才回退读取仓库旁边的 gitignored 覆盖文件。LLM 维度由 `llm.apiKind` 控制,默认 `openai_responses`,可设为 `openai_chat` 接旧 Chat Completions 兼容网关,或 `anthropic` 接 Anthropic Messages。
|
||||
|
||||
创意 Agent `gpt-5` 文本链路已从 APIMart 切到 VectorEngine:`api-server` 读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` 构造 OpenAI-compatible LLM client,并自动补齐 `/v1` 前缀用于 Responses 协议。排查或切换密钥后,可在本地运行:
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use module_big_fish::{
|
||||
BIG_FISH_MAX_LEVEL_COUNT, BIG_FISH_MIN_LEVEL_COUNT, BigFishAnchorPack, BigFishGameDraft,
|
||||
BigFishLevelBlueprint, BigFishRuntimeParams, compile_default_draft,
|
||||
};
|
||||
use platform_llm::{LlmClient, LlmMessage, LlmTextRequest};
|
||||
use platform_llm::{LlmClient, LlmMessage, LlmRunRequest};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
@@ -109,20 +109,20 @@ async fn request_big_fish_json_stage(
|
||||
empty_response_message: &str,
|
||||
) -> Result<JsonValue, BigFishDraftCompileError> {
|
||||
let response = llm_client
|
||||
.request_text(
|
||||
LlmTextRequest::new(vec![
|
||||
.run(
|
||||
LlmRunRequest::new(vec![
|
||||
LlmMessage::system(BIG_FISH_DRAFT_JSON_ONLY_SYSTEM_PROMPT),
|
||||
LlmMessage::user(user_prompt),
|
||||
])
|
||||
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
||||
.with_responses_api()
|
||||
.with_openai_responses()
|
||||
.with_web_search(true),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
BigFishDraftCompileError::new(format!("{debug_label} LLM 请求失败:{error}"))
|
||||
})?;
|
||||
let text = response.content.trim();
|
||||
let text = response.text.trim();
|
||||
if text.is_empty() {
|
||||
return Err(BigFishDraftCompileError::new(empty_response_message));
|
||||
}
|
||||
@@ -130,15 +130,15 @@ async fn request_big_fish_json_stage(
|
||||
Ok(value) => Ok(value),
|
||||
Err(_) => {
|
||||
let repaired = llm_client
|
||||
.request_text(
|
||||
LlmTextRequest::new(vec![
|
||||
.run(
|
||||
LlmRunRequest::new(vec![
|
||||
LlmMessage::system(BIG_FISH_DRAFT_JSON_REPAIR_SYSTEM_PROMPT),
|
||||
LlmMessage::user(format!(
|
||||
"请把下面这段文本修复成单个合法 JSON 对象,不要补充额外解释:\n\n{text}"
|
||||
)),
|
||||
])
|
||||
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
||||
.with_responses_api(),
|
||||
.with_openai_responses(),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
@@ -146,7 +146,7 @@ async fn request_big_fish_json_stage(
|
||||
"{debug_label} JSON 修复请求失败:{error}"
|
||||
))
|
||||
})?;
|
||||
parse_json_response_text(repaired.content.as_str()).map_err(|error| {
|
||||
parse_json_response_text(repaired.text.as_str()).map_err(|error| {
|
||||
BigFishDraftCompileError::new(format!("{debug_label} JSON 解析失败:{error}"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use platform_llm::{LlmClient, LlmError, LlmMessage, LlmStreamDelta, LlmTextRequest};
|
||||
use platform_llm::{LlmClient, LlmError, LlmMessage, LlmStreamDelta, LlmRunRequest};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use crate::llm_model_routing::CREATION_TEMPLATE_LLM_MODEL;
|
||||
@@ -150,7 +150,7 @@ where
|
||||
F: FnMut(&str),
|
||||
{
|
||||
let response = llm_client
|
||||
.stream_text(
|
||||
.stream_run(
|
||||
build_creation_agent_llm_request(system_prompt, user_prompt, enable_web_search),
|
||||
|delta: &LlmStreamDelta| {
|
||||
if !emit_reply_updates {
|
||||
@@ -167,7 +167,7 @@ where
|
||||
)
|
||||
.await
|
||||
.map_err(CreationAgentJsonTurnFailure::Stream)?;
|
||||
let parsed = parse_json_response_text(response.content.as_str())
|
||||
let parsed = parse_json_response_text(response.text.as_str())
|
||||
.map_err(|_| CreationAgentJsonTurnFailure::Parse)?;
|
||||
|
||||
Ok(CreationAgentJsonTurnOutput { parsed })
|
||||
@@ -184,14 +184,14 @@ fn build_creation_agent_llm_request(
|
||||
system_prompt: String,
|
||||
user_prompt: String,
|
||||
enable_web_search: bool,
|
||||
) -> LlmTextRequest {
|
||||
) -> LlmRunRequest {
|
||||
// 创作 Agent 是否联网由 api-server 配置集中传入,避免各玩法各自散落默认值。
|
||||
LlmTextRequest::new(vec![
|
||||
LlmRunRequest::new(vec![
|
||||
LlmMessage::system(system_prompt),
|
||||
LlmMessage::user(user_prompt),
|
||||
])
|
||||
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
||||
.with_responses_api()
|
||||
.with_openai_responses()
|
||||
.with_web_search(enable_web_search)
|
||||
.with_request_timeout_ms(CREATION_AGENT_STREAM_REQUEST_TIMEOUT_MS)
|
||||
}
|
||||
@@ -203,17 +203,17 @@ pub(crate) async fn request_creation_agent_json_turn<E>(
|
||||
build_error: impl Fn(String) -> E,
|
||||
) -> Result<JsonValue, E> {
|
||||
let response = llm_client
|
||||
.request_text(
|
||||
LlmTextRequest::new(vec![
|
||||
.run(
|
||||
LlmRunRequest::new(vec![
|
||||
LlmMessage::system(system_prompt),
|
||||
LlmMessage::user(user_prompt),
|
||||
])
|
||||
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
||||
.with_responses_api(),
|
||||
.with_openai_responses(),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| build_error(error.to_string()))?;
|
||||
parse_json_response_text(response.content.as_str())
|
||||
parse_json_response_text(response.text.as_str())
|
||||
.map_err(|error| build_error(error.to_string()))
|
||||
}
|
||||
|
||||
@@ -331,7 +331,7 @@ mod tests {
|
||||
|
||||
assert!(request.enable_web_search);
|
||||
assert_eq!(request.model.as_deref(), Some(CREATION_TEMPLATE_LLM_MODEL));
|
||||
assert_eq!(request.protocol, platform_llm::LlmTextProtocol::Responses);
|
||||
assert_eq!(request.api_kind, platform_llm::LlmApiKind::OpenAiResponses);
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
assert_eq!(
|
||||
request.request_timeout_ms,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use platform_llm::{LlmClient, LlmMessage, LlmTextRequest};
|
||||
use platform_llm::{LlmClient, LlmMessage, LlmRunRequest};
|
||||
use serde_json::{Map as JsonMap, Value as JsonValue};
|
||||
use shared_contracts::runtime::ExecuteCustomWorldAgentActionRequest;
|
||||
|
||||
@@ -94,18 +94,18 @@ pub async fn generate_custom_world_agent_entities(
|
||||
};
|
||||
|
||||
let response = llm_client
|
||||
.request_text(
|
||||
LlmTextRequest::new(vec![
|
||||
.run(
|
||||
LlmRunRequest::new(vec![
|
||||
LlmMessage::system(system_prompt),
|
||||
LlmMessage::user(user_prompt),
|
||||
])
|
||||
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
||||
.with_responses_api()
|
||||
.with_openai_responses()
|
||||
.with_web_search(true),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| format!("{action} LLM 请求失败:{error}"))?;
|
||||
let generated_entities = parse_json_array_response(response.content.as_str())
|
||||
let generated_entities = parse_json_array_response(response.text.as_str())
|
||||
.map_err(|error| format!("{action} JSON 解析失败:{error}"))?;
|
||||
let normalized_entities =
|
||||
normalize_generated_entities(action, generated_entities, draft_profile, count);
|
||||
|
||||
@@ -17,7 +17,7 @@ use module_assets::{
|
||||
AssetObjectAccessPolicy, AssetObjectFieldError, build_asset_entity_binding_input,
|
||||
build_asset_object_upsert_input, generate_asset_binding_id, generate_asset_object_id,
|
||||
};
|
||||
use platform_llm::{LlmMessage, LlmTextRequest};
|
||||
use platform_llm::{LlmMessage, LlmRunRequest};
|
||||
use platform_oss::{
|
||||
LegacyAssetPrefix, OssHeadObjectRequest, OssObjectAccess, OssSignedGetObjectUrlRequest,
|
||||
};
|
||||
@@ -1132,19 +1132,19 @@ async fn generate_entity_with_fallback(state: &AppState, profile: &Value, kind:
|
||||
let Some(llm_client) = state.llm_client() else {
|
||||
return fallback;
|
||||
};
|
||||
let request = LlmTextRequest::new(vec![
|
||||
let request = LlmRunRequest::new(vec![
|
||||
LlmMessage::system(build_result_entity_system_prompt()),
|
||||
LlmMessage::user(build_result_entity_user_prompt(profile, kind, &fallback)),
|
||||
])
|
||||
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
||||
.with_responses_api()
|
||||
.with_openai_responses()
|
||||
.with_web_search(true);
|
||||
|
||||
llm_client
|
||||
.request_text(request)
|
||||
.run(request)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|response| serde_json::from_str::<Value>(response.content.trim()).ok())
|
||||
.and_then(|response| serde_json::from_str::<Value>(response.text.trim()).ok())
|
||||
.unwrap_or(fallback)
|
||||
}
|
||||
|
||||
@@ -1157,7 +1157,7 @@ async fn generate_scene_npc_with_fallback(
|
||||
let Some(llm_client) = state.llm_client() else {
|
||||
return fallback;
|
||||
};
|
||||
let request = LlmTextRequest::new(vec![
|
||||
let request = LlmRunRequest::new(vec![
|
||||
LlmMessage::system(build_result_scene_npc_system_prompt()),
|
||||
LlmMessage::user(build_result_scene_npc_user_prompt(
|
||||
profile,
|
||||
@@ -1166,14 +1166,14 @@ async fn generate_scene_npc_with_fallback(
|
||||
)),
|
||||
])
|
||||
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
||||
.with_responses_api()
|
||||
.with_openai_responses()
|
||||
.with_web_search(true);
|
||||
|
||||
llm_client
|
||||
.request_text(request)
|
||||
.run(request)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|response| serde_json::from_str::<Value>(response.content.trim()).ok())
|
||||
.and_then(|response| serde_json::from_str::<Value>(response.text.trim()).ok())
|
||||
.unwrap_or(fallback)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::prompt::foundation_draft::{
|
||||
build_custom_world_role_outline_batch_json_repair_prompt,
|
||||
build_custom_world_role_outline_batch_prompt,
|
||||
};
|
||||
use platform_llm::{LlmClient, LlmMessage, LlmTextRequest};
|
||||
use platform_llm::{LlmClient, LlmMessage, LlmRunRequest};
|
||||
use serde_json::{Map as JsonMap, Value as JsonValue, json};
|
||||
use shared_contracts::runtime::ExecuteCustomWorldAgentActionRequest;
|
||||
use spacetime_client::CustomWorldAgentSessionRecord;
|
||||
@@ -195,7 +195,7 @@ where
|
||||
enable_web_search,
|
||||
)
|
||||
.await?;
|
||||
let text = response.content.trim();
|
||||
let text = response.text.trim();
|
||||
if text.is_empty() {
|
||||
return Err(empty_response_message.to_string());
|
||||
}
|
||||
@@ -203,17 +203,17 @@ where
|
||||
Ok(value) => Ok(value),
|
||||
Err(_) => {
|
||||
let repaired = llm_client
|
||||
.request_text(
|
||||
LlmTextRequest::new(vec![
|
||||
.run(
|
||||
LlmRunRequest::new(vec![
|
||||
LlmMessage::system(FOUNDATION_JSON_REPAIR_SYSTEM_PROMPT),
|
||||
LlmMessage::user(repair_prompt_builder(text)),
|
||||
])
|
||||
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
||||
.with_responses_api(),
|
||||
.with_openai_responses(),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| format!("{repair_debug_label} LLM 请求失败:{error}"))?;
|
||||
parse_json_response_text(repaired.content.as_str())
|
||||
parse_json_response_text(repaired.text.as_str())
|
||||
.map_err(|error| format!("{repair_debug_label} JSON 解析失败:{error}"))
|
||||
}
|
||||
}
|
||||
@@ -225,7 +225,7 @@ async fn request_foundation_text_with_optional_search_fallback(
|
||||
user_prompt: &str,
|
||||
debug_label: &str,
|
||||
enable_web_search: bool,
|
||||
) -> Result<platform_llm::LlmTextResponse, String> {
|
||||
) -> Result<platform_llm::LlmRunResponse, String> {
|
||||
match request_foundation_text(llm_client, system_prompt, user_prompt, enable_web_search).await {
|
||||
Ok(response) => Ok(response),
|
||||
Err(error) if enable_web_search && should_retry_foundation_without_web_search(&error) => {
|
||||
@@ -247,15 +247,15 @@ async fn request_foundation_text(
|
||||
system_prompt: &str,
|
||||
user_prompt: &str,
|
||||
enable_web_search: bool,
|
||||
) -> Result<platform_llm::LlmTextResponse, platform_llm::LlmError> {
|
||||
) -> Result<platform_llm::LlmRunResponse, platform_llm::LlmError> {
|
||||
llm_client
|
||||
.request_text(
|
||||
LlmTextRequest::new(vec![
|
||||
.run(
|
||||
LlmRunRequest::new(vec![
|
||||
LlmMessage::system(system_prompt),
|
||||
LlmMessage::user(user_prompt),
|
||||
])
|
||||
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
||||
.with_responses_api()
|
||||
.with_openai_responses()
|
||||
.with_web_search(enable_web_search),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -7,7 +7,7 @@ use axum::{
|
||||
sse::{Event, Sse},
|
||||
},
|
||||
};
|
||||
use platform_llm::{LlmMessage, LlmMessageRole, LlmTextProtocol, LlmTextRequest};
|
||||
use platform_llm::{LlmMessage, LlmMessageRole, LlmApiKind, LlmRunRequest};
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::llm::{
|
||||
LlmChatCompletionRequest, LlmChatCompletionResponse, LlmChatMessagePayload, LlmChatMessageRole,
|
||||
@@ -33,15 +33,15 @@ pub async fn proxy_llm_chat_completions(
|
||||
)
|
||||
})?;
|
||||
|
||||
let request = LlmTextRequest {
|
||||
let request = LlmRunRequest {
|
||||
model: payload.model,
|
||||
protocol: LlmTextProtocol::ChatCompletions,
|
||||
api_kind: LlmApiKind::OpenAiChat,
|
||||
messages: payload
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(map_chat_message)
|
||||
.collect::<Vec<_>>(),
|
||||
max_tokens: None,
|
||||
max_output_tokens: None,
|
||||
enable_web_search: false,
|
||||
request_timeout_ms: None,
|
||||
};
|
||||
@@ -51,7 +51,7 @@ pub async fn proxy_llm_chat_completions(
|
||||
}
|
||||
|
||||
let response = llm_client
|
||||
.request_text(request)
|
||||
.run(request)
|
||||
.await
|
||||
.map_err(|error| llm_error_response(&request_context, map_llm_error(error)))?;
|
||||
|
||||
@@ -60,7 +60,7 @@ pub async fn proxy_llm_chat_completions(
|
||||
LlmChatCompletionResponse {
|
||||
id: response.response_id,
|
||||
model: response.model,
|
||||
content: response.content,
|
||||
content: response.text,
|
||||
finish_reason: response.finish_reason,
|
||||
},
|
||||
)
|
||||
@@ -69,11 +69,11 @@ pub async fn proxy_llm_chat_completions(
|
||||
|
||||
fn stream_llm_chat_completions(
|
||||
llm_client: platform_llm::LlmClient,
|
||||
request: LlmTextRequest,
|
||||
request: LlmRunRequest,
|
||||
) -> Sse<impl tokio_stream::Stream<Item = Result<Event, Infallible>>> {
|
||||
let stream = async_stream::stream! {
|
||||
let (delta_tx, mut delta_rx) = tokio::sync::mpsc::unbounded_channel::<Value>();
|
||||
let llm_stream = llm_client.stream_text(request, move |delta| {
|
||||
let llm_stream = llm_client.stream_run(request, move |delta| {
|
||||
let _ = delta_tx.send(json!({
|
||||
"delta": delta.delta_text,
|
||||
"content": delta.accumulated_text,
|
||||
@@ -105,7 +105,7 @@ fn stream_llm_chat_completions(
|
||||
json!(LlmChatCompletionResponse {
|
||||
id: response.response_id,
|
||||
model: response.model,
|
||||
content: response.content,
|
||||
content: response.text,
|
||||
finish_reason: response.finish_reason,
|
||||
}),
|
||||
));
|
||||
@@ -182,7 +182,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn llm_chat_completions_returns_non_stream_text_payload() {
|
||||
async fn llm_chat_completions_returns_non_stream_run_payload() {
|
||||
let server_url = spawn_mock_server(vec![MockResponse {
|
||||
status_line: "200 OK",
|
||||
content_type: "application/json; charset=utf-8",
|
||||
|
||||
@@ -21,7 +21,7 @@ use module_match3d::{
|
||||
MATCH3D_MESSAGE_ID_PREFIX, MATCH3D_PROFILE_ID_PREFIX, MATCH3D_RUN_ID_PREFIX,
|
||||
MATCH3D_SESSION_ID_PREFIX,
|
||||
};
|
||||
use platform_llm::{LlmMessage, LlmTextRequest};
|
||||
use platform_llm::{LlmMessage, LlmRunRequest};
|
||||
use platform_oss::{LegacyAssetPrefix, OssObjectAccess, OssPutObjectRequest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -870,18 +870,18 @@ async fn generate_match3d_draft_plan(
|
||||
config.theme_text, gameplay_item_count, generated_item_count
|
||||
);
|
||||
let response = llm_client
|
||||
.request_text(
|
||||
LlmTextRequest::new(vec![
|
||||
.run(
|
||||
LlmRunRequest::new(vec![
|
||||
LlmMessage::system(system_prompt),
|
||||
LlmMessage::user(user_prompt),
|
||||
])
|
||||
.with_model(MATCH3D_WORK_METADATA_LLM_MODEL)
|
||||
.with_responses_api(),
|
||||
.with_openai_responses(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(response) => parse_match3d_draft_plan(response.content.as_str(), config)
|
||||
Ok(response) => parse_match3d_draft_plan(response.text.as_str(), config)
|
||||
.unwrap_or_else(|| fallback_match3d_draft_plan(config)),
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
|
||||
@@ -92,19 +92,19 @@ pub(super) async fn request_match3d_work_tags_with_llm(
|
||||
summary.unwrap_or_default()
|
||||
);
|
||||
let response = llm_client
|
||||
.request_text(
|
||||
LlmTextRequest::new(vec![
|
||||
.run(
|
||||
LlmRunRequest::new(vec![
|
||||
LlmMessage::system("你是抓大鹅作品标签编辑,只返回 JSON 字符串数组。"),
|
||||
LlmMessage::user(user_prompt),
|
||||
])
|
||||
.with_model(MATCH3D_WORK_METADATA_LLM_MODEL)
|
||||
.with_responses_api(),
|
||||
.with_openai_responses(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
let tags = parse_match3d_tags_from_text(response.content.as_str());
|
||||
let tags = parse_match3d_tags_from_text(response.text.as_str());
|
||||
if tags.len() >= MATCH3D_MIN_GENERATED_TAG_COUNT {
|
||||
return Some(tags);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use platform_llm::{LlmMessage, LlmTextRequest};
|
||||
use platform_llm::{LlmMessage, LlmRunRequest};
|
||||
use serde_json::{Value as JsonValue, json};
|
||||
use shared_contracts::visual_novel::{VisualNovelResultDraft, VisualNovelRuntimeStep};
|
||||
|
||||
@@ -285,36 +285,36 @@ pub(crate) fn build_visual_novel_repair_user_prompt(
|
||||
pub(crate) fn build_visual_novel_creation_llm_request(
|
||||
params: VisualNovelCreationPromptParams<'_>,
|
||||
enable_web_search: bool,
|
||||
) -> LlmTextRequest {
|
||||
LlmTextRequest::new(vec![
|
||||
) -> LlmRunRequest {
|
||||
LlmRunRequest::new(vec![
|
||||
LlmMessage::system(VISUAL_NOVEL_CREATION_SYSTEM_PROMPT),
|
||||
LlmMessage::user(build_visual_novel_creation_user_prompt(params)),
|
||||
])
|
||||
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
||||
.with_responses_api()
|
||||
.with_openai_responses()
|
||||
.with_web_search(enable_web_search)
|
||||
}
|
||||
|
||||
pub(crate) fn build_visual_novel_runtime_llm_request(
|
||||
params: VisualNovelRuntimePromptParams<'_>,
|
||||
) -> LlmTextRequest {
|
||||
LlmTextRequest::new(vec![
|
||||
) -> LlmRunRequest {
|
||||
LlmRunRequest::new(vec![
|
||||
LlmMessage::system(VISUAL_NOVEL_RUNTIME_GM_SYSTEM_PROMPT),
|
||||
LlmMessage::user(build_visual_novel_runtime_user_prompt(params)),
|
||||
])
|
||||
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
||||
.with_responses_api()
|
||||
.with_openai_responses()
|
||||
}
|
||||
|
||||
pub(crate) fn build_visual_novel_repair_llm_request(
|
||||
params: VisualNovelRepairPromptParams<'_>,
|
||||
) -> LlmTextRequest {
|
||||
LlmTextRequest::new(vec![
|
||||
) -> LlmRunRequest {
|
||||
LlmRunRequest::new(vec![
|
||||
LlmMessage::system(VISUAL_NOVEL_REPAIR_SYSTEM_PROMPT),
|
||||
LlmMessage::user(build_visual_novel_repair_user_prompt(params)),
|
||||
])
|
||||
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
||||
.with_responses_api()
|
||||
.with_openai_responses()
|
||||
}
|
||||
|
||||
pub(crate) fn visual_novel_tool_descriptors() -> Vec<VisualNovelToolDescriptor> {
|
||||
@@ -451,7 +451,7 @@ fn strip_json_code_fence(text: &str) -> &str {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use platform_llm::LlmTextProtocol;
|
||||
use platform_llm::LlmApiKind;
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
@@ -661,7 +661,7 @@ mod tests {
|
||||
creation_request.model.as_deref(),
|
||||
Some(CREATION_TEMPLATE_LLM_MODEL)
|
||||
);
|
||||
assert_eq!(creation_request.protocol, LlmTextProtocol::Responses);
|
||||
assert_eq!(creation_request.api_kind, LlmApiKind::OpenAiResponses);
|
||||
assert!(creation_request.enable_web_search);
|
||||
assert!(
|
||||
creation_request.messages[0]
|
||||
@@ -687,7 +687,7 @@ mod tests {
|
||||
runtime_request.model.as_deref(),
|
||||
Some(CREATION_TEMPLATE_LLM_MODEL)
|
||||
);
|
||||
assert_eq!(runtime_request.protocol, LlmTextProtocol::Responses);
|
||||
assert_eq!(runtime_request.api_kind, LlmApiKind::OpenAiResponses);
|
||||
assert!(!runtime_request.enable_web_search);
|
||||
assert!(
|
||||
runtime_request.messages[0]
|
||||
|
||||
@@ -19,7 +19,7 @@ use module_assets::{
|
||||
build_asset_object_upsert_input, generate_asset_binding_id, generate_asset_object_id,
|
||||
};
|
||||
use module_puzzle::{PuzzleGeneratedImageCandidate, PuzzleRuntimeLevelStatus};
|
||||
use platform_llm::{LlmMessage, LlmMessageContentPart, LlmTextRequest};
|
||||
use platform_llm::{LlmMessage, LlmMessageContentPart, LlmRunRequest};
|
||||
use platform_oss::{LegacyAssetPrefix, OssSignedGetObjectUrlRequest};
|
||||
use platform_oss::{OssHeadObjectRequest, OssObjectAccess, OssPutObjectRequest};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -705,18 +705,18 @@ pub(crate) async fn generate_puzzle_first_level_name(
|
||||
if let Some(llm_client) = state.llm_client() {
|
||||
let user_prompt = build_puzzle_first_level_name_user_prompt(picture_description);
|
||||
let response = llm_client
|
||||
.request_text(
|
||||
LlmTextRequest::new(vec![
|
||||
.run(
|
||||
LlmRunRequest::new(vec![
|
||||
LlmMessage::system(PUZZLE_FIRST_LEVEL_NAME_SYSTEM_PROMPT),
|
||||
LlmMessage::user(user_prompt),
|
||||
])
|
||||
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
||||
.with_responses_api(),
|
||||
.with_openai_responses(),
|
||||
)
|
||||
.await;
|
||||
match response {
|
||||
Ok(response) => {
|
||||
if let Some(naming) = parse_puzzle_level_naming_from_text(response.content.as_str())
|
||||
if let Some(naming) = parse_puzzle_level_naming_from_text(response.text.as_str())
|
||||
{
|
||||
return naming;
|
||||
}
|
||||
@@ -758,8 +758,8 @@ pub(crate) async fn generate_puzzle_first_level_name_from_image(
|
||||
};
|
||||
let user_text = build_puzzle_first_level_name_vision_user_text(picture_description);
|
||||
let response = llm_client
|
||||
.request_text(
|
||||
LlmTextRequest::new(vec![
|
||||
.run(
|
||||
LlmRunRequest::new(vec![
|
||||
LlmMessage::system(PUZZLE_FIRST_LEVEL_NAME_SYSTEM_PROMPT),
|
||||
LlmMessage::user_multimodal(vec![
|
||||
LlmMessageContentPart::InputText { text: user_text },
|
||||
@@ -769,13 +769,13 @@ pub(crate) async fn generate_puzzle_first_level_name_from_image(
|
||||
]),
|
||||
])
|
||||
.with_model(PUZZLE_LEVEL_NAME_VISION_LLM_MODEL)
|
||||
.with_max_tokens(PUZZLE_LEVEL_NAME_VISION_MAX_TOKENS),
|
||||
.with_max_output_tokens(PUZZLE_LEVEL_NAME_VISION_MAX_TOKENS),
|
||||
)
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
parse_puzzle_level_naming_from_text(response.content.as_str()).or_else(|| {
|
||||
parse_puzzle_level_naming_from_text(response.text.as_str()).or_else(|| {
|
||||
tracing::warn!(
|
||||
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
model = PUZZLE_LEVEL_NAME_VISION_LLM_MODEL,
|
||||
|
||||
@@ -8,19 +8,19 @@ pub(super) async fn generate_puzzle_work_tags(
|
||||
if let Some(llm_client) = state.llm_client() {
|
||||
let user_prompt = build_puzzle_tag_generation_user_prompt(work_title, work_description);
|
||||
let response = llm_client
|
||||
.request_text(
|
||||
LlmTextRequest::new(vec![
|
||||
.run(
|
||||
LlmRunRequest::new(vec![
|
||||
LlmMessage::system(PUZZLE_TAG_GENERATION_SYSTEM_PROMPT),
|
||||
LlmMessage::user(user_prompt),
|
||||
])
|
||||
.with_model(CREATION_TEMPLATE_LLM_MODEL)
|
||||
.with_responses_api(),
|
||||
.with_openai_responses(),
|
||||
)
|
||||
.await;
|
||||
match response {
|
||||
Ok(response) => {
|
||||
let tags = normalize_puzzle_tag_candidates(parse_puzzle_tags_from_text(
|
||||
response.content.as_str(),
|
||||
response.text.as_str(),
|
||||
));
|
||||
if tags.len() == module_puzzle::PUZZLE_MAX_TAG_COUNT {
|
||||
return tags;
|
||||
|
||||
@@ -7,7 +7,7 @@ use axum::{
|
||||
sse::{Event, Sse},
|
||||
},
|
||||
};
|
||||
use platform_llm::{LlmMessage, LlmTextRequest};
|
||||
use platform_llm::{LlmMessage, LlmRunRequest};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::story::StoryRuntimeSnapshotPayload as RuntimeStorySnapshotPayload;
|
||||
@@ -232,22 +232,22 @@ where
|
||||
};
|
||||
|
||||
let reply_prompt = build_npc_chat_turn_reply_prompt(&prompt_input);
|
||||
let mut reply_request = LlmTextRequest::new(vec![
|
||||
let mut reply_request = LlmRunRequest::new(vec![
|
||||
LlmMessage::system(NPC_CHAT_TURN_REPLY_SYSTEM_PROMPT),
|
||||
LlmMessage::user(reply_prompt),
|
||||
])
|
||||
.with_chat_completions_api();
|
||||
reply_request.max_tokens = Some(700);
|
||||
.with_openai_chat();
|
||||
reply_request.max_output_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());
|
||||
|
||||
let reply_response = llm_client
|
||||
.stream_text(reply_request, |delta| {
|
||||
.stream_run(reply_request, |delta| {
|
||||
on_reply_update(delta.accumulated_text.as_str());
|
||||
})
|
||||
.await
|
||||
.ok()?;
|
||||
let npc_reply = normalize_required_text(reply_response.content.as_str()).unwrap_or_else(|| {
|
||||
let npc_reply = normalize_required_text(reply_response.text.as_str()).unwrap_or_else(|| {
|
||||
build_deterministic_npc_reply(
|
||||
npc_name,
|
||||
payload.player_message.as_str(),
|
||||
@@ -261,19 +261,19 @@ where
|
||||
|
||||
let suggestion_prompt =
|
||||
build_npc_chat_turn_suggestion_prompt(&prompt_input, npc_reply.as_str());
|
||||
let mut suggestion_request = LlmTextRequest::new(vec![
|
||||
let mut suggestion_request = LlmRunRequest::new(vec![
|
||||
LlmMessage::system(NPC_CHAT_TURN_SUGGESTION_SYSTEM_PROMPT),
|
||||
LlmMessage::user(suggestion_prompt),
|
||||
])
|
||||
.with_chat_completions_api();
|
||||
suggestion_request.max_tokens = Some(200);
|
||||
.with_openai_chat();
|
||||
suggestion_request.max_output_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());
|
||||
let suggestion_text = llm_client
|
||||
.request_text(suggestion_request)
|
||||
.run(suggestion_request)
|
||||
.await
|
||||
.ok()
|
||||
.map(|response| response.content)
|
||||
.map(|response| response.text)
|
||||
.unwrap_or_default();
|
||||
let (mut suggestions, mut function_suggestions, should_end_chat) =
|
||||
parse_npc_chat_suggestion_resolution(
|
||||
|
||||
@@ -7,7 +7,7 @@ use axum::{
|
||||
sse::{Event, Sse},
|
||||
},
|
||||
};
|
||||
use platform_llm::{LlmMessage, LlmTextRequest};
|
||||
use platform_llm::{LlmMessage, LlmRunRequest};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::story::StoryRuntimeSnapshotPayload as RuntimeStorySnapshotPayload;
|
||||
@@ -582,20 +582,20 @@ async fn request_runtime_plain_text(
|
||||
return fallback_text.unwrap_or_default();
|
||||
};
|
||||
|
||||
let mut request = LlmTextRequest::new(vec![
|
||||
let mut request = LlmRunRequest::new(vec![
|
||||
LlmMessage::system(system_prompt),
|
||||
LlmMessage::user(user_prompt),
|
||||
])
|
||||
.with_chat_completions_api();
|
||||
request.max_tokens = Some(400);
|
||||
.with_openai_chat();
|
||||
request.max_output_tokens = Some(400);
|
||||
request.enable_web_search = state.config.rpg_llm_web_search_enabled;
|
||||
request.model = Some(RPG_STORY_LLM_MODEL.to_string());
|
||||
|
||||
llm_client
|
||||
.request_text(request)
|
||||
.run(request)
|
||||
.await
|
||||
.ok()
|
||||
.map(|response| response.content.trim().to_string())
|
||||
.map(|response| response.text.trim().to_string())
|
||||
.filter(|text| !text.is_empty())
|
||||
.or(fallback_text)
|
||||
.unwrap_or_default()
|
||||
@@ -615,22 +615,22 @@ fn stream_plain_text_response<'a>(
|
||||
return;
|
||||
};
|
||||
|
||||
let mut request = LlmTextRequest::new(vec![
|
||||
let mut request = LlmRunRequest::new(vec![
|
||||
LlmMessage::system(system_prompt),
|
||||
LlmMessage::user(user_prompt),
|
||||
])
|
||||
.with_chat_completions_api();
|
||||
request.max_tokens = Some(700);
|
||||
.with_openai_chat();
|
||||
request.max_output_tokens = Some(700);
|
||||
request.enable_web_search = enable_web_search;
|
||||
request.model = Some(RPG_STORY_LLM_MODEL.to_string());
|
||||
|
||||
let response = llm_client
|
||||
.stream_text(request, |_| {})
|
||||
.stream_run(request, |_| {})
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
let final_text = response.content.trim();
|
||||
let final_text = response.text.trim();
|
||||
let output = if final_text.is_empty() {
|
||||
fallback_text.as_str()
|
||||
} else {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user