feat(codex): 支持 Yunyi 等代理的 API Key 认证和 responses API

## 主要改进

### 1. 增强 API Key 字段兼容性
- 支持多种字段名:api_key, apiKey, OPENAI_API_KEY
- 兼容 Codex CLI 和 Yunyi 等代理的凭证格式

### 2. 修复健康检查逻辑
- 自定义 base_url 时使用 Codex responses API 格式
- 官方 OpenAI 时使用 chat/completions API
- 正确处理 Yunyi 等代理的 500 错误响应

### 3. 改进错误提示
- 提供更友好的配置错误提示信息

## 技术细节
- codex.rs: 添加 OPENAI_API_KEY 别名支持
- provider_pool_service.rs: 根据 base_url 选择正确的 API 格式

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
jiesen
2025-12-21 15:22:15 +07:00
co-authored by Claude Sonnet 4.5
parent fe542ccbf3
commit 4b34a8f57f
2 changed files with 68 additions and 8 deletions
+7 -1
View File
@@ -51,7 +51,13 @@ pub struct CodexCredentials {
)]
pub refresh_token: Option<String>,
/// API Key(Codex CLI 支持通过 API Key 登录)
#[serde(default, skip_serializing_if = "Option::is_none", alias = "apiKey")]
/// 支持字段名: api_key, apiKey, OPENAI_API_KEY
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "apiKey",
alias = "OPENAI_API_KEY"
)]
pub api_key: Option<String>,
/// API Base URL(可选)
#[serde(default, skip_serializing_if = "Option::is_none", alias = "apiBaseUrl")]
@@ -837,6 +837,7 @@ impl ProviderPoolService {
}
// Codex 健康检查
// 支持 Yunyi 等代理使用 responses API 格式
async fn check_codex_health(&self, creds_path: &str, model: &str) -> Result<(), String> {
use crate::providers::codex::CodexProvider;
@@ -846,13 +847,14 @@ impl ProviderPoolService {
.await
.map_err(|e| format!("加载 Codex 凭证失败: {}", e))?;
let token = provider
.ensure_valid_token()
.await
.map_err(|e| format!("获取 Codex Token 失败: {}", e))?;
let token = provider.ensure_valid_token().await.map_err(|e| {
format!(
"获取 Codex Token 失败: 配置错误,请检查凭证设置。详情:{}",
e
)
})?;
// 使用 OpenAI 兼容 API 进行健康检查
// 兼容 Codex CLI API Key 模式:如果 auth.json 提供 api_base_url,则优先使用
// 获取 base_url,如果设置了则使用 responses API,否则使用 OpenAI chat/completions
let base_url = provider
.credentials
.api_base_url
@@ -860,7 +862,59 @@ impl ProviderPoolService {
.map(|s| s.trim())
.filter(|s| !s.is_empty());
self.check_openai_health(&token, base_url, model).await
match base_url {
Some(base) => {
// 使用自定义 base_url (如 Yunyi),使用 responses API 格式
let base = base.trim_end_matches('/');
let url = if base.ends_with("/v1") {
format!("{}/responses", base)
} else {
format!("{}/v1/responses", base)
};
// Codex/Yunyi 使用 responses API 格式
let request_body = serde_json::json!({
"model": model,
"input": "Say OK",
"max_output_tokens": 10
});
tracing::debug!(
"[HEALTH_CHECK] Codex responses API URL: {}, model: {}",
url,
model
);
let response = self
.client
.post(&url)
.bearer_auth(&token)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.header("Openai-Beta", "responses=experimental")
.json(&request_body)
.timeout(self.health_check_timeout)
.send()
.await
.map_err(|e| format!("请求失败: {}", e))?;
if response.status().is_success() {
Ok(())
} else {
let status = response.status();
let body = response.text().await.unwrap_or_default();
Err(format!(
"HTTP {} - {}",
status,
body.chars().take(200).collect::<String>()
))
}
}
None => {
// 没有自定义 base_url,使用 OpenAI 官方 chat/completions API
self.check_openai_health(&token, None, model).await
}
}
}
// Claude OAuth 健康检查