diff --git a/backend/crates/providers/openai/src/credential/admin.rs b/backend/crates/providers/openai/src/credential/admin.rs index 7c76d1541..6469603ac 100644 --- a/backend/crates/providers/openai/src/credential/admin.rs +++ b/backend/crates/providers/openai/src/credential/admin.rs @@ -1288,9 +1288,9 @@ fn parse_oauth_import_tokens( Value::Object(object) => { for (key, value) in object { let token = match key.as_str() { - "accessToken" => &mut access_token, - "refreshToken" => &mut refresh_token, - "idToken" => &mut id_token, + "accessToken" | "access_token" => &mut access_token, + "refreshToken" | "refresh_token" => &mut refresh_token, + "idToken" | "id_token" => &mut id_token, _ => { pending.push(value); continue; diff --git a/backend/crates/providers/openai/tests/credential/admin.rs b/backend/crates/providers/openai/tests/credential/admin.rs index 9c2657d06..4caffbace 100644 --- a/backend/crates/providers/openai/tests/credential/admin.rs +++ b/backend/crates/providers/openai/tests/credential/admin.rs @@ -59,6 +59,48 @@ async fn direct_import_persists_opaque_tokens_without_profile_or_token_validatio assert!(account.account.next_refresh_at().is_none()); } +#[tokio::test] +async fn direct_import_accepts_snake_case_oauth_token_aliases() { + let service = CodexCredentialAdminService::new( + Arc::new(UnusedRefresher), + Arc::new(TestLeaseCoordinator::default()), + runtime_policy(), + ); + let prepared = service + .prepare_import_document(serde_json::json!({ + "accounts": [{ + "platform": "openai", + "type": "oauth", + "credentials": { + "access_token": "snake-access-token", + "refresh_token": "snake-refresh-token", + "id_token": "snake-id-token" + } + }] + })) + .await + .expect("snake_case OAuth token aliases must be accepted"); + + let account = prepared.accounts().first().expect("one prepared account"); + let runtime = CodexCredentialCodec::decode(&account.credential).expect("stored credential"); + let oauth = runtime.authentication.oauth().expect("OAuth credential"); + assert_eq!( + ( + oauth.access_token.expose_secret(), + oauth + .refresh_token + .as_ref() + .map(|token| token.expose_secret()), + oauth.id_token.as_ref().map(|token| token.expose_secret()), + ), + ( + "snake-access-token", + Some("snake-refresh-token"), + Some("snake-id-token"), + ) + ); +} + #[tokio::test] async fn direct_import_projects_access_token_jwt_expiry_without_persisting_refresh_margin() { let expires_at = Utc diff --git a/backend/crates/providers/openai/tests/transport/diagnostics.rs b/backend/crates/providers/openai/tests/transport/diagnostics.rs index 6b2b44660..fac97b6fb 100644 --- a/backend/crates/providers/openai/tests/transport/diagnostics.rs +++ b/backend/crates/providers/openai/tests/transport/diagnostics.rs @@ -195,7 +195,7 @@ fn bare_http_429_remains_a_temporary_rate_limit() { } #[test] -fn sub2api_openai_account_failure_matrix_is_preserved() { +fn openai_account_failure_matrix_is_preserved() { let cases = [ ( 400, diff --git a/backend/crates/providers/xai/src/credential/repository.rs b/backend/crates/providers/xai/src/credential/repository.rs index a66e90a30..799385ea4 100644 --- a/backend/crates/providers/xai/src/credential/repository.rs +++ b/backend/crates/providers/xai/src/credential/repository.rs @@ -237,19 +237,19 @@ impl GrokCredentialAdmin { let mut secret = decode_secret(&loaded.credential)?; let mut credentials = serde_json::Map::new(); credentials.insert( - "access_token".to_owned(), + "accessToken".to_owned(), Value::String(std::mem::take(&mut secret.access_token)), ); credentials.insert( - "refresh_token".to_owned(), + "refreshToken".to_owned(), Value::String(std::mem::take(&mut secret.refresh_token)), ); if let Some(id_token) = secret.id_token.take() { - credentials.insert("id_token".to_owned(), Value::String(id_token)); + credentials.insert("idToken".to_owned(), Value::String(id_token)); } - credentials.insert("token_type".to_owned(), Value::String("Bearer".to_owned())); + credentials.insert("tokenType".to_owned(), Value::String("Bearer".to_owned())); credentials.insert( - "expires_at".to_owned(), + "expiresAt".to_owned(), Value::String( loaded .account @@ -260,11 +260,11 @@ impl GrokCredentialAdmin { ), ); credentials.insert( - "base_url".to_owned(), + "baseUrl".to_owned(), Value::String(GROK_CLI_BASE_URL.to_owned()), ); credentials.insert( - "client_id".to_owned(), + "clientId".to_owned(), Value::String(OFFICIAL_CLIENT_ID.to_owned()), ); credentials.insert( @@ -290,7 +290,7 @@ impl GrokCredentialAdmin { Ok(GrokAccountExport(serde_json::json!({ "version": 1, "type": "oauth-account-bundle", - "exported_at": exported_at.to_rfc3339(), + "exportedAt": exported_at.to_rfc3339(), "accounts": exported_accounts, "proxies": [], }))) diff --git a/backend/crates/providers/xai/src/provider.rs b/backend/crates/providers/xai/src/provider.rs index 67403ed49..a186caeca 100644 --- a/backend/crates/providers/xai/src/provider.rs +++ b/backend/crates/providers/xai/src/provider.rs @@ -2076,7 +2076,7 @@ const WORKER_LEASE_RENEWAL: Duration = Duration::from_secs(5 * 60); const OAUTH_REFRESH_INTERVAL: Duration = Duration::from_secs(30); const QUOTA_CATALOG_INTERVAL: Duration = Duration::from_secs(5 * 60); // rolling 24h 描述的是上游用量窗口,不代表从本次观测起封禁 24 小时; -// 缺少可信 reset 时间时按 sub2api 的短周期探测策略恢复检查。 +// 缺少可信 reset 时间时按短周期探测策略恢复检查。 const EXHAUSTED_QUOTA_FALLBACK_RECHECK_INTERVAL: Duration = Duration::from_secs(10 * 60); const EXHAUSTED_QUOTA_REFRESH_RETRY_INTERVAL: Duration = QUOTA_CATALOG_INTERVAL; const CLI_RELEASE_WORKER_OWNER: &str = "xai-cli-release"; diff --git a/backend/crates/providers/xai/src/transport/compaction.rs b/backend/crates/providers/xai/src/transport/compaction.rs index 80b251b52..d9b841ae7 100644 --- a/backend/crates/providers/xai/src/transport/compaction.rs +++ b/backend/crates/providers/xai/src/transport/compaction.rs @@ -108,7 +108,7 @@ impl GrokCompactionRequest { self.reasoning_replay_session_id.as_deref() } - /// 返回经 sub2api 别名表归一化后的 xAI wire 模型。 + /// 返回归一化后的 xAI wire 模型。 pub(crate) fn upstream_model(&self) -> Option<&str> { self.body.get("model").and_then(Value::as_str) } @@ -286,8 +286,7 @@ pub(crate) fn compaction_wire_events( Value::Null, ); let item = compaction_item(summary, encrypted_content); - // sub2api 的 compact contract 以真实密文为成功依据,不向客户端延续 - // 摘要生成阶段的 incomplete 状态。 + // compact 成功以真实密文为依据,不向客户端延续摘要生成阶段的 incomplete 状态。 let terminal_response = compaction_response( completed, terminal_source.or(created_source), diff --git a/backend/crates/providers/xai/src/transport/request.rs b/backend/crates/providers/xai/src/transport/request.rs index 889c6ff1b..64e86ecd5 100644 --- a/backend/crates/providers/xai/src/transport/request.rs +++ b/backend/crates/providers/xai/src/transport/request.rs @@ -91,7 +91,7 @@ impl GrokResponsesRequest { self.reasoning_replay_session_id.as_deref() } - /// 返回经 sub2api 别名表归一化后的 xAI wire 模型。 + /// 返回归一化后的 xAI wire 模型。 pub(crate) fn upstream_model(&self) -> Option<&str> { self.body.get("model").and_then(Value::as_str) } @@ -1928,8 +1928,8 @@ impl ToolNormalizer { "apply_patch" => self.normalize_apply_patch_tool(tool), "x_search" | "collections_search" | "file_search" | "code_execution" | "code_interpreter" => Ok(vec![Value::Object(without_defer_loading(tool))]), - // sub2api 的 xAI Responses 适配层对未获 xAI 接受的工具做静默过滤, - // 并在过滤后同步收敛 tool_choice;不要把客户端可选扩展升级成整单 400。 + // 对未获 xAI 接受的工具做静默过滤,并在过滤后同步收敛 tool_choice; + // 不要把客户端可选扩展升级成整单 400。 "" | "computer_use_preview" | "image_generation" => Ok(Vec::new()), _ => Ok(Vec::new()), } @@ -3517,15 +3517,15 @@ fn normalize_compaction_input( .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()); - let has_sub2api_shape = item.contains_key("summary") + let has_structured_compaction_shape = item.contains_key("summary") || item.contains_key("id") || item.contains_key("status") || string_field(item, "type").trim() == "compaction_summary"; // 旧版 adapter 曾将明文摘要直接写入 encrypted_content,且不带 - // id/status/summary。仅对这一可识别的历史形态保留明文恢复;新形态严格按 - // sub2api 回放为 xAI reasoning 密文 + 可见 conversation summary。 - if !has_sub2api_shape { + // id/status/summary。仅对这一可识别的历史形态保留明文恢复;新形态严格回放为 + // xAI reasoning 密文 + 可见 conversation summary。 + if !has_structured_compaction_shape { let Some(summary) = encrypted else { return Ok(Vec::new()); }; diff --git a/backend/crates/providers/xai/tests/credential/contract.rs b/backend/crates/providers/xai/tests/credential/contract.rs index 515f571f9..4aa8ee1f3 100644 --- a/backend/crates/providers/xai/tests/credential/contract.rs +++ b/backend/crates/providers/xai/tests/credential/contract.rs @@ -363,20 +363,48 @@ async fn oauth_bundle_export_is_provider_owned_canonical_and_debug_redacted() { let value = export.into_value(); assert_eq!(value["version"], 1); assert_eq!(value["type"], "oauth-account-bundle"); + assert!(value.get("exportedAt").is_some()); + assert!(value.get("exported_at").is_none()); assert_eq!(value["accounts"][0]["platform"], "grok"); assert_eq!(value["accounts"][0]["type"], "oauth"); assert_eq!( - value["accounts"][0]["credentials"]["base_url"], + value["accounts"][0]["credentials"]["baseUrl"], provider_xai::GROK_CLI_BASE_URL ); assert_eq!( - value["accounts"][0]["credentials"]["access_token"], + value["accounts"][0]["credentials"]["accessToken"], "access-export" ); + assert_eq!( + value["accounts"][0]["credentials"]["refreshToken"], + "refresh-export" + ); + assert_eq!(value["accounts"][0]["credentials"]["idToken"], "id-export"); + assert_eq!(value["accounts"][0]["credentials"]["tokenType"], "Bearer"); + assert!( + value["accounts"][0]["credentials"]["expiresAt"] + .as_str() + .is_some() + ); + assert_eq!( + value["accounts"][0]["credentials"]["clientId"], + provider_xai::OFFICIAL_CLIENT_ID + ); assert_eq!( value["accounts"][0]["credentials"]["scope"], "openid profile email offline_access grok-cli:access api:access" ); + for field in [ + "access_token", + "refresh_token", + "id_token", + "token_type", + "expires_at", + "base_url", + "client_id", + ] { + assert!(value["accounts"][0]["credentials"].get(field).is_none()); + } assert_eq!(value["proxies"], serde_json::json!([])); } diff --git a/backend/crates/providers/xai/tests/credential/import.rs b/backend/crates/providers/xai/tests/credential/import.rs index b192ecda7..0f32ad5b3 100644 --- a/backend/crates/providers/xai/tests/credential/import.rs +++ b/backend/crates/providers/xai/tests/credential/import.rs @@ -225,7 +225,7 @@ fn oauth_account_document_should_reject_api_key_credential_field() { fn oauth_account_document_should_ignore_source_wrapper_and_non_auth_metadata() { let mut document: serde_json::Value = serde_json::from_slice(&oauth_account_document()).expect("fixture JSON"); - document["type"] = serde_json::Value::String("sub2api-data".to_owned()); + document["type"] = serde_json::Value::String("external-oauth-data".to_owned()); document["version"] = serde_json::Value::from(99); document["proxies"] = serde_json::json!([{"url": "http://127.0.0.1:8080"}]); document["source_extension"] = serde_json::json!({"opaque": true}); diff --git a/backend/crates/providers/xai/tests/provider/contract.rs b/backend/crates/providers/xai/tests/provider/contract.rs index 5624e4a78..9136e446a 100644 --- a/backend/crates/providers/xai/tests/provider/contract.rs +++ b/backend/crates/providers/xai/tests/provider/contract.rs @@ -1534,7 +1534,7 @@ async fn compaction_wire_should_exclude_reasoning_from_summary_content() { } #[tokio::test] -async fn compaction_stream_should_accept_short_summary_like_sub2api() { +async fn compaction_stream_should_accept_short_summary() { let transport = StubInferenceTransport::sequence([InferenceMode::SuccessBody(compaction_sse( "too short", None, diff --git a/backend/crates/providers/xai/tests/transport/compaction.rs b/backend/crates/providers/xai/tests/transport/compaction.rs index 994b04c71..7dea55242 100644 --- a/backend/crates/providers/xai/tests/transport/compaction.rs +++ b/backend/crates/providers/xai/tests/transport/compaction.rs @@ -76,7 +76,7 @@ fn encoder_should_preserve_history_order_and_append_summary_prompt() { } #[test] -fn encoder_should_preserve_sub2api_compaction_constraints() { +fn encoder_should_preserve_compaction_constraints() { let request = encode(json!({ "model": "client-model", "input": [ @@ -190,7 +190,7 @@ fn encoder_should_consume_only_the_terminal_compaction_trigger() { } #[test] -fn decoder_should_preserve_sub2api_summary_text_after_trimming_edges() { +fn decoder_should_preserve_summary_text_after_trimming_edges() { let raw = valid_summary("checkpoint-only-secret"); let summary = decode_summary(&format!(" {raw} \n")).expect("summary"); @@ -234,7 +234,7 @@ fn decoder_should_preserve_unclosed_leading_analysis() { } #[test] -fn decoder_should_preserve_compaction_control_tokens_like_sub2api() { +fn decoder_should_preserve_compaction_control_tokens() { let raw = valid_summary( "Quoted and , and , \ plus and .", @@ -278,7 +278,7 @@ fn decoder_should_preserve_numbered_sections_that_quote_analysis_close() { } #[test] -fn decoder_should_accept_short_summary_like_sub2api() { +fn decoder_should_accept_short_summary() { let summary = decode_summary("too short").expect("short summary"); assert_eq!(summary, "too short"); diff --git a/backend/crates/providers/xai/tests/transport/network.rs b/backend/crates/providers/xai/tests/transport/network.rs index 53ff961fe..f843a1bcd 100644 --- a/backend/crates/providers/xai/tests/transport/network.rs +++ b/backend/crates/providers/xai/tests/transport/network.rs @@ -567,7 +567,7 @@ async fn inference_transport_should_classify_free_usage_429_as_account_free_quot } #[tokio::test] -async fn inference_transport_should_apply_sub2api_body_aware_400_failures() { +async fn inference_transport_should_apply_body_aware_400_failures() { let cases = [ ( json!({"error": {"code": "subscription:free-usage-exhausted", "message": "free usage exhausted"}}), diff --git a/backend/crates/providers/xai/tests/transport/request.rs b/backend/crates/providers/xai/tests/transport/request.rs index c0d9e21c4..2784bbde5 100644 --- a/backend/crates/providers/xai/tests/transport/request.rs +++ b/backend/crates/providers/xai/tests/transport/request.rs @@ -198,7 +198,7 @@ fn encoder_should_apply_build_defaults_and_normalize_reasoning_effort() { } #[test] -fn encoder_should_strip_sub2api_grok_unsupported_fields() { +fn encoder_should_strip_grok_unsupported_fields() { let request = raw_request(json!({ "model": "client-model", "input": "hello", @@ -943,7 +943,7 @@ fn custom_apply_patch_declaration_should_use_patch_parameter() { } #[test] -fn hosted_tool_choice_should_preserve_the_sub2api_specific_choice() { +fn hosted_tool_choice_should_preserve_normalized_web_search_choice() { let request = raw_request(json!({ "model": "client", "input": "search", @@ -1244,7 +1244,7 @@ fn compaction_history_should_become_plaintext_user_continuation_in_place() { } #[test] -fn sub2api_compaction_history_should_restore_reasoning_ciphertext_and_visible_summary() { +fn structured_compaction_history_should_restore_reasoning_ciphertext_and_visible_summary() { for item_type in ["compaction", "compaction_summary"] { let request = raw_request(json!({ "model": "client", @@ -1264,7 +1264,7 @@ fn sub2api_compaction_history_should_restore_reasoning_ciphertext_and_visible_su })); let encoded = GrokResponsesRequest::encode(&request, "grok-4.5", &client_key()) - .expect("sub2api compaction continuation"); + .expect("structured compaction continuation"); let body = Value::Object(encoded.body().clone()); assert_eq!( @@ -1290,7 +1290,7 @@ fn sub2api_compaction_history_should_restore_reasoning_ciphertext_and_visible_su } #[test] -fn malformed_compaction_history_should_be_dropped_like_sub2api() { +fn malformed_compaction_history_should_be_dropped() { for encrypted_content in [ None, Some(json!(null)),