mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
fix(kiro): 修复流响应识别与号池配额刷新 Token 续期问题 (#322)
* fix(kiro): 兼容 application/json 头下的 eventstream 流响应 - stream_pump: 对带 kiro:generateAssistantResponse envelope 的上游响应按流处理,不再仅依赖 text/event-stream 头判断\n- tests: 补充 application/json 头下仍识别为 Kiro stream 的回归测试 * fix(kiro): 修复号池配额刷新误判封禁
This commit is contained in:
@@ -60,7 +60,7 @@ pub(crate) fn build_direct_execution_frame_stream(
|
||||
let mut stream_terminal_observer = StreamingStandardTerminalObserver::default();
|
||||
let mut observer_buffered = Vec::new();
|
||||
|
||||
if !response_headers_indicate_sse(&headers) {
|
||||
if !should_treat_upstream_response_as_stream(&headers, &observer_context) {
|
||||
let original_headers = headers.clone();
|
||||
match buffer_non_sse_upstream_body(response, started_at).await {
|
||||
Ok(buffered) => {
|
||||
@@ -388,6 +388,20 @@ fn response_headers_indicate_sse(headers: &BTreeMap<String, String>) -> bool {
|
||||
.is_some_and(|value| value.to_ascii_lowercase().contains("text/event-stream"))
|
||||
}
|
||||
|
||||
fn should_treat_upstream_response_as_stream(
|
||||
headers: &BTreeMap<String, String>,
|
||||
report_context: &Value,
|
||||
) -> bool {
|
||||
if response_headers_indicate_sse(headers) {
|
||||
return true;
|
||||
}
|
||||
|
||||
report_context
|
||||
.get("envelope_name")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|value| value.eq_ignore_ascii_case(crate::ai_pipeline::KIRO_ENVELOPE_NAME))
|
||||
}
|
||||
|
||||
async fn buffer_non_sse_upstream_body(
|
||||
response: DirectUpstreamResponse,
|
||||
started_at: Instant,
|
||||
@@ -648,7 +662,7 @@ mod tests {
|
||||
use serde_json::Value;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use super::build_direct_execution_frame_stream;
|
||||
use super::{build_direct_execution_frame_stream, should_treat_upstream_response_as_stream};
|
||||
use crate::execution_runtime::transport::{
|
||||
execute_stream_plan_via_local_tunnel, DirectSyncExecutionRuntime, DirectUpstreamResponse,
|
||||
};
|
||||
@@ -666,6 +680,19 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn treats_kiro_eventstream_envelope_as_stream_even_when_content_type_is_json() {
|
||||
let headers = BTreeMap::from([("content-type".into(), "application/json".into())]);
|
||||
let report_context = serde_json::json!({
|
||||
"envelope_name": "kiro:generateAssistantResponse",
|
||||
});
|
||||
|
||||
assert!(should_treat_upstream_response_as_stream(
|
||||
&headers,
|
||||
&report_context
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn direct_execution_frame_stream_reports_ttfb_after_first_upstream_chunk() {
|
||||
let listener = crate::test_support::bind_loopback_listener()
|
||||
|
||||
@@ -8,15 +8,83 @@ use super::shared::{
|
||||
persist_provider_quota_refresh_state, quota_refresh_success_invalid_state,
|
||||
ProviderQuotaExecutionOutcome,
|
||||
};
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminLocalOAuthRefreshError};
|
||||
use crate::GatewayError;
|
||||
use aether_contracts::ProxySnapshot;
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||
};
|
||||
use aether_provider_transport::kiro::build_kiro_request_auth_from_config;
|
||||
use aether_provider_transport::{CachedOAuthEntry, LocalResolvedOAuthRequestAuth};
|
||||
use serde_json::json;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn kiro_quota_error_is_token_invalid(detail: Option<&str>) -> bool {
|
||||
let Some(detail) = detail else {
|
||||
return false;
|
||||
};
|
||||
let normalized = detail.to_ascii_lowercase();
|
||||
normalized.contains("bearer token invalid")
|
||||
|| normalized.contains("bearer token invild")
|
||||
|| normalized.contains("bearer token is invalid")
|
||||
|| normalized.contains("invalid bearer token")
|
||||
|| normalized.contains("token expired")
|
||||
|| normalized.contains("token has expired")
|
||||
|| normalized.contains("expired token")
|
||||
}
|
||||
|
||||
fn kiro_quota_error_is_account_banned(detail: Option<&str>) -> bool {
|
||||
let Some(detail) = detail else {
|
||||
return false;
|
||||
};
|
||||
let normalized = detail.to_ascii_lowercase();
|
||||
[
|
||||
"account suspended",
|
||||
"account is suspended",
|
||||
"account banned",
|
||||
"account is banned",
|
||||
"terms of service",
|
||||
"封禁",
|
||||
"封号",
|
||||
"被封",
|
||||
"账户已封禁",
|
||||
]
|
||||
.iter()
|
||||
.any(|keyword| normalized.contains(keyword))
|
||||
}
|
||||
|
||||
fn kiro_auth_from_refreshed_entry(
|
||||
entry: &CachedOAuthEntry,
|
||||
) -> Option<LocalResolvedOAuthRequestAuth> {
|
||||
if !entry.provider_type.trim().eq_ignore_ascii_case("kiro") {
|
||||
return None;
|
||||
}
|
||||
let auth_config = entry
|
||||
.metadata
|
||||
.as_ref()
|
||||
.and_then(aether_provider_transport::kiro::KiroAuthConfig::from_json_value)?;
|
||||
let auth = build_kiro_request_auth_from_config(auth_config, None)?;
|
||||
Some(LocalResolvedOAuthRequestAuth::Kiro(auth))
|
||||
}
|
||||
|
||||
fn kiro_quota_refresh_failure_status(err: &AdminLocalOAuthRefreshError) -> Option<u16> {
|
||||
match err {
|
||||
AdminLocalOAuthRefreshError::HttpStatus { status_code, .. } => Some(*status_code),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn kiro_quota_refresh_failure_message(err: &AdminLocalOAuthRefreshError) -> String {
|
||||
match err {
|
||||
AdminLocalOAuthRefreshError::HttpStatus {
|
||||
status_code,
|
||||
body_excerpt,
|
||||
..
|
||||
} => format!("Kiro Token 刷新失败 ({status_code}): {body_excerpt}"),
|
||||
_ => format!("Kiro Token 刷新失败: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn refresh_kiro_provider_quota_locally(
|
||||
state: &AdminAppState<'_>,
|
||||
provider: &StoredProviderCatalogProvider,
|
||||
@@ -46,18 +114,52 @@ pub(crate) async fn refresh_kiro_provider_quota_locally(
|
||||
}
|
||||
};
|
||||
|
||||
let Some(auth) = state
|
||||
.resolve_local_oauth_kiro_request_auth(&transport)
|
||||
.await?
|
||||
else {
|
||||
failed_count += 1;
|
||||
results.push(json!({
|
||||
"key_id": key.id,
|
||||
"key_name": key.name,
|
||||
"status": "error",
|
||||
"message": "缺少 Kiro 认证配置 (auth_config)",
|
||||
}));
|
||||
continue;
|
||||
let auth = match state.force_local_oauth_refresh_entry(&transport).await {
|
||||
Ok(Some(entry)) => match kiro_auth_from_refreshed_entry(&entry) {
|
||||
Some(LocalResolvedOAuthRequestAuth::Kiro(auth)) => auth,
|
||||
_ => {
|
||||
failed_count += 1;
|
||||
results.push(json!({
|
||||
"key_id": key.id,
|
||||
"key_name": key.name,
|
||||
"status": "error",
|
||||
"message": "Kiro Token 刷新成功但认证信息解析失败",
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
},
|
||||
Ok(None) => match state
|
||||
.resolve_local_oauth_kiro_request_auth(&transport)
|
||||
.await?
|
||||
{
|
||||
Some(auth) => auth,
|
||||
None => {
|
||||
failed_count += 1;
|
||||
results.push(json!({
|
||||
"key_id": key.id,
|
||||
"key_name": key.name,
|
||||
"status": "error",
|
||||
"message": "缺少 Kiro 认证配置 (auth_config)",
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
failed_count += 1;
|
||||
let mut payload = serde_json::Map::new();
|
||||
payload.insert("key_id".to_string(), json!(key.id));
|
||||
payload.insert("key_name".to_string(), json!(key.name));
|
||||
payload.insert("status".to_string(), json!("error"));
|
||||
payload.insert(
|
||||
"message".to_string(),
|
||||
json!(kiro_quota_refresh_failure_message(&err)),
|
||||
);
|
||||
if let Some(status_code) = kiro_quota_refresh_failure_status(&err) {
|
||||
payload.insert("status_code".to_string(), json!(status_code));
|
||||
}
|
||||
results.push(serde_json::Value::Object(payload));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let result =
|
||||
@@ -152,17 +254,22 @@ pub(crate) async fn refresh_kiro_provider_quota_locally(
|
||||
.clone()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| format!("HTTP {}", result.status_code));
|
||||
oauth_invalid_at_unix_secs = Some(now_unix_secs);
|
||||
oauth_invalid_reason = Some(format!("账户已封禁: {reason}"));
|
||||
metadata_update = Some(json!({
|
||||
"kiro": {
|
||||
"is_banned": true,
|
||||
"ban_reason": reason,
|
||||
"banned_at": now_unix_secs,
|
||||
"updated_at": now_unix_secs,
|
||||
}
|
||||
}));
|
||||
status = "banned".to_string();
|
||||
if kiro_quota_error_is_token_invalid(err_msg.as_deref()) {
|
||||
oauth_invalid_at_unix_secs = Some(now_unix_secs);
|
||||
oauth_invalid_reason = Some("Kiro Token 无效或已过期".to_string());
|
||||
} else if kiro_quota_error_is_account_banned(err_msg.as_deref()) {
|
||||
oauth_invalid_at_unix_secs = Some(now_unix_secs);
|
||||
oauth_invalid_reason = Some(format!("账户已封禁: {reason}"));
|
||||
metadata_update = Some(json!({
|
||||
"kiro": {
|
||||
"is_banned": true,
|
||||
"ban_reason": reason,
|
||||
"banned_at": now_unix_secs,
|
||||
"updated_at": now_unix_secs,
|
||||
}
|
||||
}));
|
||||
status = "banned".to_string();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -227,3 +334,32 @@ pub(crate) async fn refresh_kiro_provider_quota_locally(
|
||||
"auto_removed": 0,
|
||||
})))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{kiro_quota_error_is_account_banned, kiro_quota_error_is_token_invalid};
|
||||
|
||||
#[test]
|
||||
fn bearer_token_invalid_is_not_classified_as_banned() {
|
||||
let detail = Some("Bearer token invalid");
|
||||
|
||||
assert!(kiro_quota_error_is_token_invalid(detail));
|
||||
assert!(!kiro_quota_error_is_account_banned(detail));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearer_token_invild_typo_is_not_classified_as_banned() {
|
||||
let detail = Some("bearer token invild");
|
||||
|
||||
assert!(kiro_quota_error_is_token_invalid(detail));
|
||||
assert!(!kiro_quota_error_is_account_banned(detail));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_kiro_account_suspension_is_classified_as_banned() {
|
||||
let detail = Some("account suspended due to Terms of Service violation");
|
||||
|
||||
assert!(!kiro_quota_error_is_token_invalid(detail));
|
||||
assert!(kiro_quota_error_is_account_banned(detail));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user