mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(kiro): 放行可刷新 OAuth 调度候选 (#340)
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
use aether_admin::provider::pool as admin_provider_pool_pure;
|
use aether_admin::provider::{
|
||||||
|
pool as admin_provider_pool_pure, status as admin_provider_status_pure,
|
||||||
|
};
|
||||||
use aether_data_contracts::repository::candidates::StoredRequestCandidate;
|
use aether_data_contracts::repository::candidates::StoredRequestCandidate;
|
||||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
|
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
|
||||||
use aether_scheduler_core::{
|
use aether_scheduler_core::{
|
||||||
@@ -294,13 +296,19 @@ fn read_key_oauth_invalid_map(
|
|||||||
.map(|candidate| {
|
.map(|candidate| {
|
||||||
let oauth_invalid = provider_key_rpm_states
|
let oauth_invalid = provider_key_rpm_states
|
||||||
.get(candidate.key_id.as_str())
|
.get(candidate.key_id.as_str())
|
||||||
.is_some_and(|key| key_requires_oauth_reauth(key, now_unix_secs));
|
.is_some_and(|key| {
|
||||||
|
key_requires_oauth_reauth(key, candidate.provider_type.as_str(), now_unix_secs)
|
||||||
|
});
|
||||||
(candidate.key_id.clone(), oauth_invalid)
|
(candidate.key_id.clone(), oauth_invalid)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn key_requires_oauth_reauth(key: &StoredProviderCatalogKey, now_unix_secs: u64) -> bool {
|
fn key_requires_oauth_reauth(
|
||||||
|
key: &StoredProviderCatalogKey,
|
||||||
|
provider_type: &str,
|
||||||
|
now_unix_secs: u64,
|
||||||
|
) -> bool {
|
||||||
if !key.auth_type.trim().eq_ignore_ascii_case("oauth") {
|
if !key.auth_type.trim().eq_ignore_ascii_case("oauth") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -311,11 +319,46 @@ fn key_requires_oauth_reauth(key: &StoredProviderCatalogKey, now_unix_secs: u64)
|
|||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if !invalid_reason.is_empty() {
|
if !invalid_reason.is_empty() {
|
||||||
return !invalid_reason.starts_with("[REQUEST_FAILED]");
|
return oauth_invalid_reason_requires_reauth(key, provider_type, invalid_reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
key.expires_at_unix_secs
|
key.expires_at_unix_secs
|
||||||
.is_some_and(|value| value > 0 && value <= now_unix_secs)
|
.is_some_and(|value| value > 0 && value <= now_unix_secs)
|
||||||
|
&& !kiro_key_has_refreshable_session(key, provider_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oauth_invalid_reason_requires_reauth(
|
||||||
|
key: &StoredProviderCatalogKey,
|
||||||
|
provider_type: &str,
|
||||||
|
invalid_reason: &str,
|
||||||
|
) -> bool {
|
||||||
|
if invalid_reason.starts_with("[REQUEST_FAILED]") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let account_state = admin_provider_status_pure::resolve_pool_account_state(
|
||||||
|
Some(provider_type),
|
||||||
|
key.upstream_metadata.as_ref(),
|
||||||
|
Some(invalid_reason),
|
||||||
|
);
|
||||||
|
if account_state.blocked && !account_state.recoverable {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if invalid_reason.starts_with("[REFRESH_FAILED]")
|
||||||
|
|| invalid_reason.starts_with("[ACCOUNT_BLOCK]")
|
||||||
|
|| invalid_reason.starts_with("[OAUTH_EXPIRED]")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
!kiro_key_has_refreshable_session(key, provider_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kiro_key_has_refreshable_session(key: &StoredProviderCatalogKey, provider_type: &str) -> bool {
|
||||||
|
provider_type.trim().eq_ignore_ascii_case("kiro")
|
||||||
|
&& key
|
||||||
|
.encrypted_auth_config
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.is_some_and(|value| !value.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_provider_key_rpm_reset_at_map(
|
fn read_provider_key_rpm_reset_at_map(
|
||||||
|
|||||||
@@ -1378,6 +1378,242 @@ async fn keeps_request_failed_oauth_candidate_selectable() {
|
|||||||
assert!(skipped.is_empty());
|
assert!(skipped.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn keeps_refreshable_kiro_candidate_selectable_with_runtime_oauth_invalid_marker() {
|
||||||
|
let mut row = sample_row();
|
||||||
|
row.provider_id = "provider-kiro".to_string();
|
||||||
|
row.provider_name = "kiro".to_string();
|
||||||
|
row.provider_type = "kiro".to_string();
|
||||||
|
row.endpoint_id = "endpoint-kiro".to_string();
|
||||||
|
row.key_id = "key-kiro".to_string();
|
||||||
|
row.key_name = "kiro-refreshable".to_string();
|
||||||
|
row.key_auth_type = "oauth".to_string();
|
||||||
|
|
||||||
|
let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
||||||
|
row,
|
||||||
|
]));
|
||||||
|
let mut provider = sample_provider("provider-kiro", None);
|
||||||
|
provider.provider_type = "kiro".to_string();
|
||||||
|
let provider_catalog = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||||
|
vec![provider],
|
||||||
|
Vec::new(),
|
||||||
|
vec![{
|
||||||
|
let mut key = sample_key("key-kiro", "provider-kiro", Some(10));
|
||||||
|
key.auth_type = "oauth".to_string();
|
||||||
|
key.encrypted_auth_config = Some("encrypted-refreshable-session".to_string());
|
||||||
|
key.oauth_invalid_at_unix_secs = Some(1_710_000_000);
|
||||||
|
key.oauth_invalid_reason = Some("Kiro Token 无效或已过期".to_string());
|
||||||
|
key
|
||||||
|
}],
|
||||||
|
));
|
||||||
|
let quotas = Arc::new(InMemoryProviderQuotaRepository::seed(vec![]));
|
||||||
|
let request_candidates = Arc::new(InMemoryRequestCandidateRepository::seed(vec![]));
|
||||||
|
let state = AppState::new()
|
||||||
|
.expect("state should build")
|
||||||
|
.with_data_state_for_tests(
|
||||||
|
GatewayDataState::with_candidate_selection_provider_catalog_quota_and_request_candidates_for_tests(
|
||||||
|
candidates,
|
||||||
|
provider_catalog,
|
||||||
|
quotas,
|
||||||
|
request_candidates,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (selected, skipped) = collect_selectable_candidates_with_skip_reasons(
|
||||||
|
state.data.as_ref(),
|
||||||
|
&state,
|
||||||
|
"openai:chat",
|
||||||
|
"gpt-4.1",
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
1_710_000_100,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("selection should succeed");
|
||||||
|
|
||||||
|
assert_eq!(selected.len(), 1);
|
||||||
|
assert_eq!(selected[0].provider_id, "provider-kiro");
|
||||||
|
assert!(skipped.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn keeps_refreshable_kiro_candidate_selectable_when_oauth_token_expired() {
|
||||||
|
let mut row = sample_row();
|
||||||
|
row.provider_id = "provider-kiro".to_string();
|
||||||
|
row.provider_name = "kiro".to_string();
|
||||||
|
row.provider_type = "kiro".to_string();
|
||||||
|
row.endpoint_id = "endpoint-kiro".to_string();
|
||||||
|
row.key_id = "key-kiro".to_string();
|
||||||
|
row.key_name = "kiro-expired-refreshable".to_string();
|
||||||
|
row.key_auth_type = "oauth".to_string();
|
||||||
|
|
||||||
|
let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
||||||
|
row,
|
||||||
|
]));
|
||||||
|
let mut provider = sample_provider("provider-kiro", None);
|
||||||
|
provider.provider_type = "kiro".to_string();
|
||||||
|
let provider_catalog = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||||
|
vec![provider],
|
||||||
|
Vec::new(),
|
||||||
|
vec![{
|
||||||
|
let mut key = sample_key("key-kiro", "provider-kiro", Some(10));
|
||||||
|
key.auth_type = "oauth".to_string();
|
||||||
|
key.encrypted_auth_config = Some("encrypted-refreshable-session".to_string());
|
||||||
|
key.expires_at_unix_secs = Some(1_710_000_000);
|
||||||
|
key
|
||||||
|
}],
|
||||||
|
));
|
||||||
|
let quotas = Arc::new(InMemoryProviderQuotaRepository::seed(vec![]));
|
||||||
|
let request_candidates = Arc::new(InMemoryRequestCandidateRepository::seed(vec![]));
|
||||||
|
let state = AppState::new()
|
||||||
|
.expect("state should build")
|
||||||
|
.with_data_state_for_tests(
|
||||||
|
GatewayDataState::with_candidate_selection_provider_catalog_quota_and_request_candidates_for_tests(
|
||||||
|
candidates,
|
||||||
|
provider_catalog,
|
||||||
|
quotas,
|
||||||
|
request_candidates,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (selected, skipped) = collect_selectable_candidates_with_skip_reasons(
|
||||||
|
state.data.as_ref(),
|
||||||
|
&state,
|
||||||
|
"openai:chat",
|
||||||
|
"gpt-4.1",
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
1_710_000_100,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("selection should succeed");
|
||||||
|
|
||||||
|
assert_eq!(selected.len(), 1);
|
||||||
|
assert_eq!(selected[0].provider_id, "provider-kiro");
|
||||||
|
assert!(skipped.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn skips_kiro_candidate_after_refresh_failure_requires_reauth() {
|
||||||
|
let mut row = sample_row();
|
||||||
|
row.provider_id = "provider-kiro".to_string();
|
||||||
|
row.provider_name = "kiro".to_string();
|
||||||
|
row.provider_type = "kiro".to_string();
|
||||||
|
row.endpoint_id = "endpoint-kiro".to_string();
|
||||||
|
row.key_id = "key-kiro".to_string();
|
||||||
|
row.key_name = "kiro-refresh-failed".to_string();
|
||||||
|
row.key_auth_type = "oauth".to_string();
|
||||||
|
|
||||||
|
let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
||||||
|
row,
|
||||||
|
]));
|
||||||
|
let mut provider = sample_provider("provider-kiro", None);
|
||||||
|
provider.provider_type = "kiro".to_string();
|
||||||
|
let provider_catalog = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||||
|
vec![provider],
|
||||||
|
Vec::new(),
|
||||||
|
vec![{
|
||||||
|
let mut key = sample_key("key-kiro", "provider-kiro", Some(10));
|
||||||
|
key.auth_type = "oauth".to_string();
|
||||||
|
key.encrypted_auth_config = Some("encrypted-refreshable-session".to_string());
|
||||||
|
key.oauth_invalid_at_unix_secs = Some(1_710_000_000);
|
||||||
|
key.oauth_invalid_reason = Some(
|
||||||
|
"[REFRESH_FAILED] Token 续期失败 (401): refresh_token 无效、已过期或已撤销,请重新登录授权"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
key
|
||||||
|
}],
|
||||||
|
));
|
||||||
|
let quotas = Arc::new(InMemoryProviderQuotaRepository::seed(vec![]));
|
||||||
|
let request_candidates = Arc::new(InMemoryRequestCandidateRepository::seed(vec![]));
|
||||||
|
let state = AppState::new()
|
||||||
|
.expect("state should build")
|
||||||
|
.with_data_state_for_tests(
|
||||||
|
GatewayDataState::with_candidate_selection_provider_catalog_quota_and_request_candidates_for_tests(
|
||||||
|
candidates,
|
||||||
|
provider_catalog,
|
||||||
|
quotas,
|
||||||
|
request_candidates,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (selected, skipped) = collect_selectable_candidates_with_skip_reasons(
|
||||||
|
state.data.as_ref(),
|
||||||
|
&state,
|
||||||
|
"openai:chat",
|
||||||
|
"gpt-4.1",
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
1_710_000_100,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("selection should succeed");
|
||||||
|
|
||||||
|
assert!(selected.is_empty());
|
||||||
|
assert_eq!(skipped.len(), 1);
|
||||||
|
assert_eq!(skipped[0].candidate.provider_id, "provider-kiro");
|
||||||
|
assert_eq!(skipped[0].skip_reason, "oauth_invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn skips_refreshable_kiro_candidate_when_oauth_marker_is_account_block() {
|
||||||
|
let mut row = sample_row();
|
||||||
|
row.provider_id = "provider-kiro".to_string();
|
||||||
|
row.provider_name = "kiro".to_string();
|
||||||
|
row.provider_type = "kiro".to_string();
|
||||||
|
row.endpoint_id = "endpoint-kiro".to_string();
|
||||||
|
row.key_id = "key-kiro".to_string();
|
||||||
|
row.key_name = "kiro-account-blocked".to_string();
|
||||||
|
row.key_auth_type = "oauth".to_string();
|
||||||
|
|
||||||
|
let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
||||||
|
row,
|
||||||
|
]));
|
||||||
|
let mut provider = sample_provider("provider-kiro", None);
|
||||||
|
provider.provider_type = "kiro".to_string();
|
||||||
|
let provider_catalog = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||||
|
vec![provider],
|
||||||
|
Vec::new(),
|
||||||
|
vec![{
|
||||||
|
let mut key = sample_key("key-kiro", "provider-kiro", Some(10));
|
||||||
|
key.auth_type = "oauth".to_string();
|
||||||
|
key.encrypted_auth_config = Some("encrypted-refreshable-session".to_string());
|
||||||
|
key.oauth_invalid_at_unix_secs = Some(1_710_000_000);
|
||||||
|
key.oauth_invalid_reason = Some("账户已封禁: account banned".to_string());
|
||||||
|
key
|
||||||
|
}],
|
||||||
|
));
|
||||||
|
let quotas = Arc::new(InMemoryProviderQuotaRepository::seed(vec![]));
|
||||||
|
let request_candidates = Arc::new(InMemoryRequestCandidateRepository::seed(vec![]));
|
||||||
|
let state = AppState::new()
|
||||||
|
.expect("state should build")
|
||||||
|
.with_data_state_for_tests(
|
||||||
|
GatewayDataState::with_candidate_selection_provider_catalog_quota_and_request_candidates_for_tests(
|
||||||
|
candidates,
|
||||||
|
provider_catalog,
|
||||||
|
quotas,
|
||||||
|
request_candidates,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (selected, skipped) = collect_selectable_candidates_with_skip_reasons(
|
||||||
|
state.data.as_ref(),
|
||||||
|
&state,
|
||||||
|
"openai:chat",
|
||||||
|
"gpt-4.1",
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
1_710_000_100,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("selection should succeed");
|
||||||
|
|
||||||
|
assert!(selected.is_empty());
|
||||||
|
assert_eq!(skipped.len(), 1);
|
||||||
|
assert_eq!(skipped[0].candidate.provider_id, "provider-kiro");
|
||||||
|
assert_eq!(skipped[0].skip_reason, "oauth_invalid");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn keeps_codex_candidate_selectable_when_exhausted_account_flag_is_disabled() {
|
async fn keeps_codex_candidate_selectable_when_exhausted_account_flag_is_disabled() {
|
||||||
let mut first = sample_row();
|
let mut first = sample_row();
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ async fn gateway_locally_denies_sync_ai_control_execute_when_opted_in_and_execut
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
payload["error"]["message"],
|
payload["error"]["message"],
|
||||||
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商(OpenAI Chat Completions,原因代码: missing_auth_context)"
|
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商"
|
||||||
);
|
);
|
||||||
assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0);
|
||||||
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
|
||||||
@@ -255,7 +255,7 @@ async fn gateway_locally_denies_stream_ai_control_execute_when_opted_in_and_exec
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
payload["error"]["message"],
|
payload["error"]["message"],
|
||||||
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商(OpenAI Chat Completions,原因代码: missing_auth_context)"
|
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商"
|
||||||
);
|
);
|
||||||
assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0);
|
||||||
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
|
||||||
@@ -393,7 +393,7 @@ async fn gateway_does_not_proxy_control_execute_over_http_when_opted_in_and_exec
|
|||||||
let payload: serde_json::Value = response.json().await.expect("body should parse");
|
let payload: serde_json::Value = response.json().await.expect("body should parse");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
payload["error"]["message"],
|
payload["error"]["message"],
|
||||||
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商(OpenAI Chat Completions,原因代码: missing_auth_context)"
|
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商"
|
||||||
);
|
);
|
||||||
assert_eq!(*plan_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*plan_hits.lock().expect("mutex should lock"), 0);
|
||||||
assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0);
|
||||||
@@ -526,7 +526,7 @@ async fn gateway_does_not_proxy_control_execute_over_http_when_opted_in_and_exec
|
|||||||
let payload: serde_json::Value = response.json().await.expect("body should parse");
|
let payload: serde_json::Value = response.json().await.expect("body should parse");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
payload["error"]["message"],
|
payload["error"]["message"],
|
||||||
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商(OpenAI Chat Completions,原因代码: missing_auth_context)"
|
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商"
|
||||||
);
|
);
|
||||||
assert_eq!(*plan_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*plan_hits.lock().expect("mutex should lock"), 0);
|
||||||
assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0);
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ async fn gateway_locally_denies_openai_chat_after_repeated_execution_runtime_mis
|
|||||||
assert_eq!(payload["error"]["type"], "http_error");
|
assert_eq!(payload["error"]["type"], "http_error");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
payload["error"]["message"],
|
payload["error"]["message"],
|
||||||
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商(OpenAI Chat Completions,原因代码: missing_auth_context)"
|
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ async fn gateway_locally_denies_openai_chat_when_control_api_is_configured_witho
|
|||||||
assert_eq!(payload["error"]["type"], "http_error");
|
assert_eq!(payload["error"]["type"], "http_error");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
payload["error"]["message"],
|
payload["error"]["message"],
|
||||||
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商(OpenAI Chat Completions,原因代码: missing_auth_context)"
|
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商"
|
||||||
);
|
);
|
||||||
assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0);
|
||||||
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
|
||||||
@@ -359,7 +359,7 @@ async fn gateway_locally_denies_openai_chat_stream_after_execution_runtime_miss_
|
|||||||
assert_eq!(payload["error"]["type"], "http_error");
|
assert_eq!(payload["error"]["type"], "http_error");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
payload["error"]["message"],
|
payload["error"]["message"],
|
||||||
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商(OpenAI Chat Completions,原因代码: missing_auth_context)"
|
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商"
|
||||||
);
|
);
|
||||||
assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0);
|
||||||
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
|
||||||
@@ -537,7 +537,7 @@ async fn gateway_locally_denies_openai_responses_after_execution_runtime_miss_wi
|
|||||||
"cli",
|
"cli",
|
||||||
"openai:cli",
|
"openai:cli",
|
||||||
"{\"model\":\"gpt-5\",\"input\":\"hello\"}",
|
"{\"model\":\"gpt-5\",\"input\":\"hello\"}",
|
||||||
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商(OpenAI Responses,原因代码: missing_auth_context)",
|
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商",
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -551,7 +551,7 @@ async fn gateway_locally_denies_claude_messages_after_execution_runtime_miss_wit
|
|||||||
"chat",
|
"chat",
|
||||||
"claude:chat",
|
"claude:chat",
|
||||||
"{\"model\":\"claude-sonnet-4-5\",\"messages\":[]}",
|
"{\"model\":\"claude-sonnet-4-5\",\"messages\":[]}",
|
||||||
"请求缺少本地执行所需的认证、模型或配置上下文,无法选择上游提供商(Claude Messages,原因代码: decision_input_unavailable)",
|
"请求缺少本地执行所需的认证、模型或配置上下文,无法选择上游提供商",
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -565,7 +565,7 @@ async fn gateway_locally_denies_openai_responses_stream_after_execution_runtime_
|
|||||||
"cli",
|
"cli",
|
||||||
"openai:cli",
|
"openai:cli",
|
||||||
"{\"model\":\"gpt-5\",\"input\":\"hello\",\"stream\":true}",
|
"{\"model\":\"gpt-5\",\"input\":\"hello\",\"stream\":true}",
|
||||||
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商(OpenAI Responses,原因代码: missing_auth_context)",
|
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商",
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -579,7 +579,7 @@ async fn gateway_locally_denies_claude_messages_stream_after_execution_runtime_m
|
|||||||
"chat",
|
"chat",
|
||||||
"claude:chat",
|
"claude:chat",
|
||||||
"{\"model\":\"claude-sonnet-4-5\",\"messages\":[],\"stream\":true}",
|
"{\"model\":\"claude-sonnet-4-5\",\"messages\":[],\"stream\":true}",
|
||||||
"请求缺少本地执行所需的认证、模型或配置上下文,无法选择上游提供商(Claude Messages,原因代码: decision_input_unavailable)",
|
"请求缺少本地执行所需的认证、模型或配置上下文,无法选择上游提供商",
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -593,7 +593,7 @@ async fn gateway_locally_denies_openai_compact_after_execution_runtime_miss_with
|
|||||||
"compact",
|
"compact",
|
||||||
"openai:compact",
|
"openai:compact",
|
||||||
"{\"model\":\"gpt-5\",\"input\":\"hello\"}",
|
"{\"model\":\"gpt-5\",\"input\":\"hello\"}",
|
||||||
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商(OpenAI Responses Compact,原因代码: missing_auth_context)",
|
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商",
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -607,7 +607,7 @@ async fn gateway_locally_denies_openai_compact_stream_after_execution_runtime_mi
|
|||||||
"compact",
|
"compact",
|
||||||
"openai:compact",
|
"openai:compact",
|
||||||
"{\"model\":\"gpt-5\",\"input\":\"hello\",\"stream\":true}",
|
"{\"model\":\"gpt-5\",\"input\":\"hello\",\"stream\":true}",
|
||||||
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商(OpenAI Responses Compact,原因代码: missing_auth_context)",
|
"请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商",
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -621,7 +621,7 @@ async fn gateway_locally_denies_gemini_generate_after_execution_runtime_miss_wit
|
|||||||
"chat",
|
"chat",
|
||||||
"gemini:chat",
|
"gemini:chat",
|
||||||
"{\"contents\":[]}",
|
"{\"contents\":[]}",
|
||||||
"请求缺少本地执行所需的认证、模型或配置上下文,无法选择上游提供商(Gemini Public,原因代码: decision_input_unavailable)",
|
"请求缺少本地执行所需的认证、模型或配置上下文,无法选择上游提供商",
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -635,7 +635,7 @@ async fn gateway_locally_denies_gemini_v1_generate_after_execution_runtime_miss_
|
|||||||
"chat",
|
"chat",
|
||||||
"gemini:chat",
|
"gemini:chat",
|
||||||
"{\"contents\":[]}",
|
"{\"contents\":[]}",
|
||||||
"请求缺少本地执行所需的认证、模型或配置上下文,无法选择上游提供商(Gemini Public,原因代码: decision_input_unavailable)",
|
"请求缺少本地执行所需的认证、模型或配置上下文,无法选择上游提供商",
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -649,7 +649,7 @@ async fn gateway_locally_denies_gemini_stream_after_execution_runtime_miss_witho
|
|||||||
"chat",
|
"chat",
|
||||||
"gemini:chat",
|
"gemini:chat",
|
||||||
"{\"contents\":[]}",
|
"{\"contents\":[]}",
|
||||||
"请求缺少本地执行所需的认证、模型或配置上下文,无法选择上游提供商(Gemini Public,原因代码: decision_input_unavailable)",
|
"请求缺少本地执行所需的认证、模型或配置上下文,无法选择上游提供商",
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -654,7 +654,7 @@ async fn gateway_surfaces_local_execution_runtime_miss_reason_when_all_openai_ch
|
|||||||
assert_eq!(payload["error"]["type"], "http_error");
|
assert_eq!(payload["error"]["type"], "http_error");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
payload["error"]["message"],
|
payload["error"]["message"],
|
||||||
"找到 1 个支持模型 gpt-5 的候选提供商,但本次同步请求全部不可用:提供商类型不支持本地执行 2 次(原因代码: all_candidates_skipped)"
|
"找到 1 个支持模型 gpt-5 的候选提供商,但本次同步请求全部不可用:提供商类型不支持本地执行 2 次"
|
||||||
);
|
);
|
||||||
|
|
||||||
let stored_candidates = request_candidate_repository
|
let stored_candidates = request_candidate_repository
|
||||||
|
|||||||
Reference in New Issue
Block a user