mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
chore(release): v0.79.0
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
use futures::StreamExt;
|
||||
use proxycast_agent::{
|
||||
convert_agent_event, AsterAgentState, SessionConfigBuilder, TauriAgentEvent,
|
||||
};
|
||||
use proxycast_core::database::dao::api_key_provider::ApiProviderType;
|
||||
use proxycast_core::database::init_database;
|
||||
use proxycast_lib::services::request_tool_policy_prompt_service::{
|
||||
merge_system_prompt_with_request_tool_policy, resolve_request_tool_policy,
|
||||
WebSearchExecutionTracker,
|
||||
};
|
||||
use proxycast_services::api_key_provider_service::ApiKeyProviderService;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn should_run_real_test() -> bool {
|
||||
std::env::var("PROXYCAST_REAL_API_TEST").ok().as_deref() == Some("1")
|
||||
}
|
||||
|
||||
fn resolve_model_name(
|
||||
explicit: Option<String>,
|
||||
provider_models: &[String],
|
||||
) -> Result<String, String> {
|
||||
if let Some(model) = explicit
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
return Ok(model.to_string());
|
||||
}
|
||||
|
||||
if let Some(model) = provider_models
|
||||
.iter()
|
||||
.map(|value| value.trim())
|
||||
.find(|value| !value.is_empty())
|
||||
{
|
||||
return Ok(model.to_string());
|
||||
}
|
||||
|
||||
Err(
|
||||
"未找到可用模型:请设置 PROXYCAST_REAL_MODEL,或在 Provider custom_models 中配置模型。"
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_codex_provider_and_model(
|
||||
db: &proxycast_core::database::DbConnection,
|
||||
) -> Result<(String, String), String> {
|
||||
let explicit_model = std::env::var("PROXYCAST_REAL_MODEL").ok();
|
||||
|
||||
if let Ok(explicit) = std::env::var("PROXYCAST_REAL_PROVIDER_ID") {
|
||||
let trimmed = explicit.trim();
|
||||
if !trimmed.is_empty() {
|
||||
let service = ApiKeyProviderService::new();
|
||||
let provider = service
|
||||
.get_provider(db, trimmed)?
|
||||
.ok_or_else(|| format!("未找到指定 Provider: {trimmed}"))?;
|
||||
let model = resolve_model_name(explicit_model, &provider.provider.custom_models)?;
|
||||
return Ok((trimmed.to_string(), model));
|
||||
}
|
||||
}
|
||||
|
||||
let service = ApiKeyProviderService::new();
|
||||
let providers = service.get_all_providers(db)?;
|
||||
providers
|
||||
.into_iter()
|
||||
.find(|item| {
|
||||
item.provider.enabled
|
||||
&& item.provider.provider_type == ApiProviderType::Codex
|
||||
&& item.api_keys.iter().any(|key| key.enabled)
|
||||
})
|
||||
.map(|item| -> Result<(String, String), String> {
|
||||
let model = resolve_model_name(explicit_model, &item.provider.custom_models)?;
|
||||
Ok((item.provider.id, model))
|
||||
})
|
||||
.transpose()?
|
||||
.ok_or_else(|| "未找到启用且含可用 Key 的 Codex Provider".to_string())
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct RealRunSummary {
|
||||
session_id: String,
|
||||
web_search: bool,
|
||||
model: String,
|
||||
tool_start_count: usize,
|
||||
tool_end_count: usize,
|
||||
web_search_tool_names: Vec<String>,
|
||||
errors: Vec<String>,
|
||||
final_text_preview: String,
|
||||
}
|
||||
|
||||
async fn run_real_case(
|
||||
state: &AsterAgentState,
|
||||
db: &proxycast_core::database::DbConnection,
|
||||
provider_id: &str,
|
||||
model_name: &str,
|
||||
web_search: bool,
|
||||
prompt: &str,
|
||||
) -> Result<RealRunSummary, String> {
|
||||
let session_id = format!("real-web-policy-{}", Uuid::new_v4());
|
||||
state
|
||||
.configure_provider_from_pool(db, provider_id, model_name, &session_id)
|
||||
.await
|
||||
.map_err(|e| format!("配置 Provider 失败: {e}"))?;
|
||||
|
||||
let agent_arc = state.get_agent_arc();
|
||||
let guard = agent_arc.read().await;
|
||||
let agent = guard.as_ref().ok_or_else(|| "Agent 未初始化".to_string())?;
|
||||
|
||||
let policy = resolve_request_tool_policy(Some(web_search), false);
|
||||
let merged_prompt = merge_system_prompt_with_request_tool_policy(None, &policy);
|
||||
let mut session_config_builder = SessionConfigBuilder::new(&session_id);
|
||||
if let Some(system_prompt) = merged_prompt {
|
||||
session_config_builder = session_config_builder.system_prompt(system_prompt);
|
||||
}
|
||||
let session_config = session_config_builder.build();
|
||||
|
||||
let user_message = aster::conversation::message::Message::user().with_text(prompt);
|
||||
let mut stream = agent
|
||||
.reply(user_message, session_config, None)
|
||||
.await
|
||||
.map_err(|e| format!("创建流式回复失败: {e}"))?;
|
||||
|
||||
let mut summary = RealRunSummary {
|
||||
session_id,
|
||||
web_search,
|
||||
model: model_name.to_string(),
|
||||
..RealRunSummary::default()
|
||||
};
|
||||
let mut tracker = WebSearchExecutionTracker::default();
|
||||
let mut text_buffer = String::new();
|
||||
|
||||
while let Some(event_result) = stream.next().await {
|
||||
match event_result {
|
||||
Ok(agent_event) => {
|
||||
for event in convert_agent_event(agent_event) {
|
||||
match &event {
|
||||
TauriAgentEvent::ToolStart {
|
||||
tool_name, tool_id, ..
|
||||
} => {
|
||||
summary.tool_start_count += 1;
|
||||
tracker.record_tool_start(&policy, tool_id, tool_name);
|
||||
if tool_name.to_ascii_lowercase().contains("websearch") {
|
||||
summary.web_search_tool_names.push(tool_name.clone());
|
||||
}
|
||||
}
|
||||
TauriAgentEvent::ToolEnd { tool_id, result } => {
|
||||
summary.tool_end_count += 1;
|
||||
tracker.record_tool_end(
|
||||
&policy,
|
||||
tool_id,
|
||||
result.success,
|
||||
result.error.as_deref(),
|
||||
);
|
||||
}
|
||||
TauriAgentEvent::TextDelta { text } => text_buffer.push_str(text),
|
||||
TauriAgentEvent::Error { message } => summary.errors.push(message.clone()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => summary.errors.push(format!("stream_error: {error}")),
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(error) = tracker.validate_web_search_requirement(&policy) {
|
||||
summary.errors.push(error);
|
||||
}
|
||||
|
||||
summary.final_text_preview = text_buffer.chars().take(280).collect();
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "真实联网测试:设置 PROXYCAST_REAL_API_TEST=1 后执行"]
|
||||
async fn test_real_gpt53_codex_web_search_scenarios() {
|
||||
if !should_run_real_test() {
|
||||
return;
|
||||
}
|
||||
|
||||
let db = init_database().expect("初始化数据库失败");
|
||||
let (provider_id, resolved_model) =
|
||||
resolve_codex_provider_and_model(&db).expect("解析 Codex Provider/模型失败");
|
||||
let model_name = std::env::var("PROXYCAST_REAL_MODEL").unwrap_or_else(|_| {
|
||||
if resolved_model.trim().is_empty() {
|
||||
"gpt-5.3-codex".to_string()
|
||||
} else {
|
||||
resolved_model
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
model_name.trim(),
|
||||
"gpt-5.3-codex",
|
||||
"本测试仅允许使用 gpt-5.3-codex"
|
||||
);
|
||||
|
||||
let state = AsterAgentState::new();
|
||||
|
||||
let scenario_a = run_real_case(
|
||||
&state,
|
||||
&db,
|
||||
&provider_id,
|
||||
&model_name,
|
||||
false,
|
||||
"场景A:webSearch=false。请简要解释什么是 Rust 的所有权模型。",
|
||||
)
|
||||
.await
|
||||
.expect("场景A调用失败");
|
||||
|
||||
println!(
|
||||
"[ScenarioA] request={{model:{}, web_search:{}, session:{}}} events={{tool_start:{}, tool_end:{}, web_search_tools:{:?}}} errors={:?} final_preview={}",
|
||||
scenario_a.model,
|
||||
scenario_a.web_search,
|
||||
scenario_a.session_id,
|
||||
scenario_a.tool_start_count,
|
||||
scenario_a.tool_end_count,
|
||||
scenario_a.web_search_tool_names,
|
||||
scenario_a.errors,
|
||||
scenario_a.final_text_preview
|
||||
);
|
||||
assert!(
|
||||
scenario_a.errors.is_empty(),
|
||||
"场景A出现错误: {:?}",
|
||||
scenario_a.errors
|
||||
);
|
||||
|
||||
let scenario_b = run_real_case(
|
||||
&state,
|
||||
&db,
|
||||
&provider_id,
|
||||
&model_name,
|
||||
true,
|
||||
"场景B:webSearch=true。请搜索并总结2026年3月4日全球重要新闻,给出来源链接。",
|
||||
)
|
||||
.await
|
||||
.expect("场景B调用失败");
|
||||
|
||||
println!(
|
||||
"[ScenarioB] request={{model:{}, web_search:{}, session:{}}} events={{tool_start:{}, tool_end:{}, web_search_tools:{:?}}} errors={:?} final_preview={}",
|
||||
scenario_b.model,
|
||||
scenario_b.web_search,
|
||||
scenario_b.session_id,
|
||||
scenario_b.tool_start_count,
|
||||
scenario_b.tool_end_count,
|
||||
scenario_b.web_search_tool_names,
|
||||
scenario_b.errors,
|
||||
scenario_b.final_text_preview
|
||||
);
|
||||
|
||||
assert!(
|
||||
scenario_b.errors.is_empty(),
|
||||
"场景B出现错误: {:?}",
|
||||
scenario_b.errors
|
||||
);
|
||||
assert!(
|
||||
scenario_b
|
||||
.web_search_tool_names
|
||||
.iter()
|
||||
.any(|name| name.to_ascii_lowercase().contains("websearch")),
|
||||
"场景B必须包含 WebSearch 工具调用,实际: {:?}",
|
||||
scenario_b.web_search_tool_names
|
||||
);
|
||||
assert!(
|
||||
scenario_b.tool_start_count > 0 && scenario_b.tool_end_count > 0,
|
||||
"场景B必须出现 tool_start/tool_end 事件,实际: start={}, end={}",
|
||||
scenario_b.tool_start_count,
|
||||
scenario_b.tool_end_count
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
use proxycast_agent::AsterAgentState;
|
||||
use proxycast_core::database::dao::api_key_provider::ApiProviderType;
|
||||
use proxycast_core::database::init_database;
|
||||
use proxycast_lib::services::request_tool_policy_prompt_service::{
|
||||
execute_web_search_preflight_if_needed, resolve_request_tool_policy, WebSearchExecutionTracker,
|
||||
};
|
||||
use proxycast_services::api_key_provider_service::ApiKeyProviderService;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn should_run_real_test() -> bool {
|
||||
std::env::var("PROXYCAST_REAL_API_TEST").ok().as_deref() == Some("1")
|
||||
}
|
||||
|
||||
fn resolve_model_name(
|
||||
explicit: Option<String>,
|
||||
provider_models: &[String],
|
||||
) -> Result<String, String> {
|
||||
if let Some(model) = explicit
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
return Ok(model.to_string());
|
||||
}
|
||||
|
||||
if let Some(model) = provider_models
|
||||
.iter()
|
||||
.map(|value| value.trim())
|
||||
.find(|value| !value.is_empty())
|
||||
{
|
||||
return Ok(model.to_string());
|
||||
}
|
||||
|
||||
Err("未找到可用模型".to_string())
|
||||
}
|
||||
|
||||
fn resolve_codex_provider_and_model(
|
||||
db: &proxycast_core::database::DbConnection,
|
||||
) -> Result<(String, String), String> {
|
||||
let explicit_model = std::env::var("PROXYCAST_REAL_MODEL").ok();
|
||||
let service = ApiKeyProviderService::new();
|
||||
let providers = service.get_all_providers(db)?;
|
||||
|
||||
providers
|
||||
.into_iter()
|
||||
.find(|item| {
|
||||
item.provider.enabled
|
||||
&& item.provider.provider_type == ApiProviderType::Codex
|
||||
&& item.api_keys.iter().any(|key| key.enabled)
|
||||
})
|
||||
.map(|item| -> Result<(String, String), String> {
|
||||
let model = resolve_model_name(explicit_model, &item.provider.custom_models)?;
|
||||
Ok((item.provider.id, model))
|
||||
})
|
||||
.transpose()?
|
||||
.ok_or_else(|| "未找到启用且含可用 Key 的 Codex Provider".to_string())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "真实联网测试:设置 PROXYCAST_REAL_API_TEST=1 后执行"]
|
||||
async fn test_real_web_search_preflight_short_input_continue() {
|
||||
if !should_run_real_test() {
|
||||
return;
|
||||
}
|
||||
|
||||
let db = init_database().expect("初始化数据库失败");
|
||||
let (provider_id, resolved_model) =
|
||||
resolve_codex_provider_and_model(&db).expect("解析 Codex Provider/模型失败");
|
||||
let model_name = std::env::var("PROXYCAST_REAL_MODEL").unwrap_or(resolved_model);
|
||||
assert_eq!(
|
||||
model_name.trim(),
|
||||
"gpt-5.3-codex",
|
||||
"本测试仅允许使用 gpt-5.3-codex"
|
||||
);
|
||||
|
||||
let state = AsterAgentState::new();
|
||||
let session_id = format!("real-web-preflight-{}", Uuid::new_v4());
|
||||
state
|
||||
.configure_provider_from_pool(&db, &provider_id, &model_name, &session_id)
|
||||
.await
|
||||
.expect("配置 Provider 失败");
|
||||
|
||||
let agent_arc = state.get_agent_arc();
|
||||
let guard = agent_arc.read().await;
|
||||
let agent = guard.as_ref().expect("Agent 未初始化");
|
||||
|
||||
let policy = resolve_request_tool_policy(Some(true), false);
|
||||
let mut tracker = WebSearchExecutionTracker::default();
|
||||
let execution = execute_web_search_preflight_if_needed(
|
||||
agent,
|
||||
&session_id,
|
||||
"继续",
|
||||
None,
|
||||
None,
|
||||
&policy,
|
||||
&mut tracker,
|
||||
)
|
||||
.await
|
||||
.expect("预调用失败");
|
||||
|
||||
let mut tool_start_count = 0usize;
|
||||
let mut tool_end_count = 0usize;
|
||||
let mut tool_names = Vec::new();
|
||||
for event in execution.events {
|
||||
match event {
|
||||
proxycast_agent::TauriAgentEvent::ToolStart { tool_name, .. } => {
|
||||
tool_start_count += 1;
|
||||
tool_names.push(tool_name);
|
||||
}
|
||||
proxycast_agent::TauriAgentEvent::ToolEnd { .. } => {
|
||||
tool_end_count += 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"[PreflightContinue] request={{model:{}, web_search:true, prompt:\"继续\", session:{}}} events={{tool_start:{}, tool_end:{}, tools:{:?}}}",
|
||||
model_name, session_id, tool_start_count, tool_end_count, tool_names
|
||||
);
|
||||
|
||||
assert!(
|
||||
tool_names
|
||||
.iter()
|
||||
.any(|name| name.to_ascii_lowercase().contains("websearch")),
|
||||
"预调用必须包含 WebSearch,实际: {:?}",
|
||||
tool_names
|
||||
);
|
||||
assert!(tool_start_count > 0, "必须出现 tool_start");
|
||||
assert!(tool_end_count > 0, "必须出现 tool_end");
|
||||
tracker
|
||||
.validate_web_search_requirement(&policy)
|
||||
.expect("预调用后应满足必需工具约束");
|
||||
}
|
||||
Reference in New Issue
Block a user