mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix: refine OpenAI error mapping
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -43,6 +43,45 @@ pub struct OpenAiErrorDetails {
|
||||
pub code: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct OpenAiErrorKind {
|
||||
error_type: &'static str,
|
||||
code: &'static str,
|
||||
}
|
||||
|
||||
const UPSTREAM_UNAVAILABLE_ERROR: OpenAiErrorKind = OpenAiErrorKind {
|
||||
error_type: "server_error",
|
||||
code: "upstream_unavailable",
|
||||
};
|
||||
const UPSTREAM_ERROR: OpenAiErrorKind = OpenAiErrorKind {
|
||||
error_type: "server_error",
|
||||
code: "upstream_error",
|
||||
};
|
||||
const INSUFFICIENT_QUOTA_ERROR: OpenAiErrorKind = OpenAiErrorKind {
|
||||
error_type: "insufficient_quota",
|
||||
code: "insufficient_quota",
|
||||
};
|
||||
const RATE_LIMIT_ERROR: OpenAiErrorKind = OpenAiErrorKind {
|
||||
error_type: "rate_limit_error",
|
||||
code: "rate_limit_exceeded",
|
||||
};
|
||||
const AUTHENTICATION_ERROR: OpenAiErrorKind = OpenAiErrorKind {
|
||||
error_type: "invalid_request_error",
|
||||
code: "invalid_api_key",
|
||||
};
|
||||
const MODEL_NOT_FOUND_ERROR: OpenAiErrorKind = OpenAiErrorKind {
|
||||
error_type: "invalid_request_error",
|
||||
code: "model_not_found",
|
||||
};
|
||||
const INVALID_UPSTREAM_RESPONSE_ERROR: OpenAiErrorKind = OpenAiErrorKind {
|
||||
error_type: "server_error",
|
||||
code: "invalid_upstream_response",
|
||||
};
|
||||
const CODEX_API_ERROR: OpenAiErrorKind = OpenAiErrorKind {
|
||||
error_type: "server_error",
|
||||
code: "codex_api_error",
|
||||
};
|
||||
|
||||
/// OpenAI 兼容错误响应。
|
||||
pub fn openai_error_response(
|
||||
status: StatusCode,
|
||||
@@ -103,12 +142,13 @@ pub fn responses_compact_dispatch_error_response(error: ResponseDispatchError) -
|
||||
}
|
||||
|
||||
pub fn responses_stream_dispatch_failed_sse_event(error: &ResponseDispatchError) -> String {
|
||||
let error = response_dispatch_http_error(
|
||||
let http_error = response_dispatch_http_error(
|
||||
error,
|
||||
ResponseDispatchStatusMode::UpstreamFailureStatus,
|
||||
ResponseDispatchMessageStyle::ResponsesStream,
|
||||
);
|
||||
responses_failed_sse_event(error.status, &error.message)
|
||||
let kind = response_dispatch_error_kind(error);
|
||||
responses_failed_sse_event(kind, &http_error.message)
|
||||
}
|
||||
|
||||
fn responses_no_available_accounts_response() -> (StatusCode, Json<Value>) {
|
||||
@@ -126,14 +166,14 @@ fn responses_no_available_accounts_response() -> (StatusCode, Json<Value>) {
|
||||
}
|
||||
|
||||
fn responses_error_response(status: StatusCode, message: &str) -> (StatusCode, Json<Value>) {
|
||||
let (error_type, code) = responses_error_type_and_code_for_status(status);
|
||||
let kind = responses_error_kind_for_status(status);
|
||||
(
|
||||
status,
|
||||
Json(json!({
|
||||
"type": "error",
|
||||
"error": {
|
||||
"type": error_type,
|
||||
"code": code,
|
||||
"type": kind.error_type,
|
||||
"code": kind.code,
|
||||
"message": message,
|
||||
}
|
||||
})),
|
||||
@@ -146,9 +186,8 @@ fn response_dispatch_openai_error_response(error: &ResponseDispatchError) -> Res
|
||||
.into_response()
|
||||
}
|
||||
|
||||
fn responses_failed_sse_event(status: StatusCode, message: &str) -> String {
|
||||
let (error_type, code) = responses_error_type_and_code_for_status(status);
|
||||
encode_response_failed_sse_event(error_type, code, message)
|
||||
fn responses_failed_sse_event(kind: OpenAiErrorKind, message: &str) -> String {
|
||||
encode_response_failed_sse_event(kind.error_type, kind.code, message)
|
||||
}
|
||||
|
||||
/// 缺失 client API key。
|
||||
@@ -192,48 +231,42 @@ fn invalid_openai_request_response(message: &str) -> (StatusCode, Json<Value>) {
|
||||
|
||||
fn chat_dispatch_openai_error(error: &ChatDispatchError) -> OpenAiErrorDetails {
|
||||
let status = chat_dispatch_status(error);
|
||||
let (message, error_type, code) = match error {
|
||||
let (message, kind) = match error {
|
||||
ChatDispatchError::NoActiveAccount | ChatDispatchError::AccountStore => (
|
||||
NO_ACTIVE_UPSTREAM_ACCOUNT_MESSAGE.to_owned(),
|
||||
"server_error",
|
||||
"upstream_unavailable",
|
||||
UPSTREAM_UNAVAILABLE_ERROR,
|
||||
),
|
||||
ChatDispatchError::Upstream(_) => (
|
||||
UPSTREAM_CODEX_REQUEST_FAILED_MESSAGE.to_owned(),
|
||||
"server_error",
|
||||
"upstream_error",
|
||||
UPSTREAM_ERROR,
|
||||
),
|
||||
ChatDispatchError::QuotaExhausted {
|
||||
count,
|
||||
upstream_error,
|
||||
} => (
|
||||
exhausted_dispatch_message(*count, "quota-exhausted", upstream_error, false),
|
||||
"server_error",
|
||||
"upstream_error",
|
||||
INSUFFICIENT_QUOTA_ERROR,
|
||||
),
|
||||
ChatDispatchError::RateLimited {
|
||||
count,
|
||||
upstream_error,
|
||||
} => (
|
||||
exhausted_dispatch_message(*count, "rate-limited", upstream_error, false),
|
||||
"server_error",
|
||||
"upstream_error",
|
||||
RATE_LIMIT_ERROR,
|
||||
),
|
||||
ChatDispatchError::Expired {
|
||||
count,
|
||||
upstream_error,
|
||||
} => (
|
||||
exhausted_dispatch_message(*count, "expired", upstream_error, false),
|
||||
"server_error",
|
||||
"upstream_error",
|
||||
AUTHENTICATION_ERROR,
|
||||
),
|
||||
ChatDispatchError::Disabled {
|
||||
count,
|
||||
upstream_error,
|
||||
} => (
|
||||
exhausted_dispatch_message(*count, "disabled", upstream_error, false),
|
||||
"server_error",
|
||||
"upstream_error",
|
||||
AUTHENTICATION_ERROR,
|
||||
),
|
||||
ChatDispatchError::Banned {
|
||||
count,
|
||||
@@ -241,45 +274,40 @@ fn chat_dispatch_openai_error(error: &ChatDispatchError) -> OpenAiErrorDetails {
|
||||
..
|
||||
} => (
|
||||
exhausted_dispatch_message(*count, "banned", upstream_error, false),
|
||||
"server_error",
|
||||
"upstream_error",
|
||||
AUTHENTICATION_ERROR,
|
||||
),
|
||||
ChatDispatchError::CloudflareChallenge {
|
||||
count,
|
||||
upstream_error,
|
||||
} => (
|
||||
exhausted_dispatch_message(*count, "cloudflare-challenge", upstream_error, false),
|
||||
"server_error",
|
||||
"upstream_error",
|
||||
UPSTREAM_ERROR,
|
||||
),
|
||||
ChatDispatchError::CloudflarePathBlocked {
|
||||
count,
|
||||
upstream_error,
|
||||
} => (
|
||||
exhausted_dispatch_message(*count, "cloudflare-path-block", upstream_error, false),
|
||||
"server_error",
|
||||
"upstream_error",
|
||||
UPSTREAM_ERROR,
|
||||
),
|
||||
ChatDispatchError::ModelUnsupported {
|
||||
count,
|
||||
upstream_error,
|
||||
} => (
|
||||
exhausted_dispatch_message(*count, "model-unsupported", upstream_error, false),
|
||||
"invalid_request_error",
|
||||
"upstream_error",
|
||||
MODEL_NOT_FOUND_ERROR,
|
||||
),
|
||||
ChatDispatchError::InvalidSse(_) | ChatDispatchError::EmptyUpstreamResponse => (
|
||||
INVALID_UPSTREAM_CODEX_RESPONSE_MESSAGE.to_owned(),
|
||||
"server_error",
|
||||
"invalid_upstream_response",
|
||||
INVALID_UPSTREAM_RESPONSE_ERROR,
|
||||
),
|
||||
};
|
||||
|
||||
OpenAiErrorDetails {
|
||||
status,
|
||||
message,
|
||||
error_type,
|
||||
code,
|
||||
error_type: kind.error_type,
|
||||
code: kind.code,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,32 +317,37 @@ fn response_dispatch_openai_error(error: &ResponseDispatchError) -> OpenAiErrorD
|
||||
ResponseDispatchStatusMode::UpstreamFailureStatus,
|
||||
ResponseDispatchMessageStyle::Standard,
|
||||
);
|
||||
let (error_type, code) = response_dispatch_error_type_and_code(error);
|
||||
let kind = response_dispatch_error_kind(error);
|
||||
|
||||
OpenAiErrorDetails {
|
||||
status,
|
||||
message,
|
||||
error_type,
|
||||
code,
|
||||
error_type: kind.error_type,
|
||||
code: kind.code,
|
||||
}
|
||||
}
|
||||
|
||||
fn response_dispatch_error_type_and_code(
|
||||
error: &ResponseDispatchError,
|
||||
) -> (&'static str, &'static str) {
|
||||
fn response_dispatch_error_kind(error: &ResponseDispatchError) -> OpenAiErrorKind {
|
||||
match error {
|
||||
ResponseDispatchError::ModelUnsupported { .. } => {
|
||||
("invalid_request_error", "upstream_error")
|
||||
ResponseDispatchError::NoActiveAccount | ResponseDispatchError::AccountStore => {
|
||||
UPSTREAM_UNAVAILABLE_ERROR
|
||||
}
|
||||
ResponseDispatchError::QuotaExhausted { .. } => INSUFFICIENT_QUOTA_ERROR,
|
||||
ResponseDispatchError::RateLimited { .. } => RATE_LIMIT_ERROR,
|
||||
ResponseDispatchError::Expired { .. }
|
||||
| ResponseDispatchError::Disabled { .. }
|
||||
| ResponseDispatchError::Banned { .. } => AUTHENTICATION_ERROR,
|
||||
ResponseDispatchError::ModelUnsupported { .. } => MODEL_NOT_FOUND_ERROR,
|
||||
ResponseDispatchError::InvalidSse(_)
|
||||
| ResponseDispatchError::MissingCompleted
|
||||
| ResponseDispatchError::EmptyUpstreamResponse => {
|
||||
("server_error", "invalid_upstream_response")
|
||||
}
|
||||
ResponseDispatchError::Failed(_) => responses_error_type_and_code_for_status(
|
||||
status_from_u16(error.http_status_code(), StatusCode::BAD_GATEWAY),
|
||||
),
|
||||
_ => ("server_error", "upstream_error"),
|
||||
| ResponseDispatchError::EmptyUpstreamResponse => INVALID_UPSTREAM_RESPONSE_ERROR,
|
||||
ResponseDispatchError::Failed(_) => responses_error_kind_for_status(status_from_u16(
|
||||
error.http_status_code(),
|
||||
StatusCode::BAD_GATEWAY,
|
||||
)),
|
||||
ResponseDispatchError::Upstream(_)
|
||||
| ResponseDispatchError::CloudflareChallenge { .. }
|
||||
| ResponseDispatchError::CloudflarePathBlocked { .. } => UPSTREAM_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,15 +362,20 @@ pub fn response_dispatch_http_error(
|
||||
}
|
||||
}
|
||||
|
||||
fn responses_error_type_and_code_for_status(status: StatusCode) -> (&'static str, &'static str) {
|
||||
fn responses_error_kind_for_status(status: StatusCode) -> OpenAiErrorKind {
|
||||
if status == StatusCode::TOO_MANY_REQUESTS {
|
||||
("rate_limit_error", "rate_limit_exceeded")
|
||||
RATE_LIMIT_ERROR
|
||||
} else if status == StatusCode::PAYMENT_REQUIRED {
|
||||
INSUFFICIENT_QUOTA_ERROR
|
||||
} else if matches!(status, StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN) {
|
||||
("invalid_request_error", "authentication_error")
|
||||
AUTHENTICATION_ERROR
|
||||
} else if status.is_client_error() {
|
||||
("invalid_request_error", "codex_api_error")
|
||||
OpenAiErrorKind {
|
||||
error_type: "invalid_request_error",
|
||||
code: "codex_api_error",
|
||||
}
|
||||
} else {
|
||||
("server_error", "codex_api_error")
|
||||
CODEX_API_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -552,7 +552,8 @@ async fn chat_completions_should_return_rate_limit_error_when_429_fallback_is_ex
|
||||
assert_eq!(status, StatusCode::TOO_MANY_REQUESTS);
|
||||
assert!(message.contains("All accounts exhausted (1 rate-limited)"));
|
||||
assert!(message.contains("rate limited"));
|
||||
assert_eq!(body["error"]["code"], "upstream_error");
|
||||
assert_eq!(body["error"]["type"], "rate_limit_error");
|
||||
assert_eq!(body["error"]["code"], "rate_limit_exceeded");
|
||||
assert_eq!(quota_state.0, 1);
|
||||
assert!(quota_state.1.is_some());
|
||||
let event = latest_usage_record(&pool, "v1.chat").await;
|
||||
@@ -683,7 +684,8 @@ async fn chat_completions_should_return_model_unsupported_error_when_fallback_is
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST);
|
||||
assert!(message.contains("All accounts exhausted (1 model-unsupported)"));
|
||||
assert!(message.contains("model_not_supported"));
|
||||
assert_eq!(body["error"]["code"], "upstream_error");
|
||||
assert_eq!(body["error"]["type"], "invalid_request_error");
|
||||
assert_eq!(body["error"]["code"], "model_not_found");
|
||||
assert_eq!(account_status.0, "active");
|
||||
}
|
||||
|
||||
@@ -787,7 +789,8 @@ async fn chat_completions_should_return_auth_error_when_401_fallback_is_exhauste
|
||||
assert_eq!(status, StatusCode::UNAUTHORIZED);
|
||||
assert!(message.contains("All accounts exhausted (1 expired)"));
|
||||
assert!(message.contains("token_revoked"));
|
||||
assert_eq!(body["error"]["code"], "upstream_error");
|
||||
assert_eq!(body["error"]["type"], "invalid_request_error");
|
||||
assert_eq!(body["error"]["code"], "invalid_api_key");
|
||||
assert_eq!(account_status.0, "expired");
|
||||
}
|
||||
|
||||
@@ -1082,7 +1085,8 @@ async fn chat_completions_should_return_quota_error_when_402_fallback_is_exhaust
|
||||
assert_eq!(status, StatusCode::PAYMENT_REQUIRED);
|
||||
assert!(message.contains("All accounts exhausted (1 quota-exhausted)"));
|
||||
assert!(message.contains("quota reached"));
|
||||
assert_eq!(body["error"]["code"], "upstream_error");
|
||||
assert_eq!(body["error"]["type"], "insufficient_quota");
|
||||
assert_eq!(body["error"]["code"], "insufficient_quota");
|
||||
assert_eq!(account_status.0, "quota_exhausted");
|
||||
}
|
||||
|
||||
|
||||
@@ -363,7 +363,8 @@ async fn responses_should_return_auth_error_when_401_fallback_is_exhausted() {
|
||||
assert_eq!(status, StatusCode::UNAUTHORIZED);
|
||||
assert!(message.contains("All accounts exhausted (1 expired)"));
|
||||
assert!(message.contains("token_revoked"));
|
||||
assert_eq!(body["error"]["code"], "upstream_error");
|
||||
assert_eq!(body["error"]["type"], "invalid_request_error");
|
||||
assert_eq!(body["error"]["code"], "invalid_api_key");
|
||||
assert_eq!(account_status.0, "expired");
|
||||
}
|
||||
|
||||
@@ -1006,6 +1007,7 @@ async fn responses_stream_should_return_model_unsupported_error_when_fallback_is
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert!(content_type.starts_with("text/event-stream"));
|
||||
assert!(body.contains("event: response.failed"));
|
||||
assert!(body.contains("\"code\":\"model_not_found\""));
|
||||
assert!(body.contains("All accounts exhausted (1 model-unsupported)"));
|
||||
assert!(body.contains("model_not_available"));
|
||||
assert_eq!(account_status.0, "active");
|
||||
@@ -1512,6 +1514,7 @@ async fn responses_stream_should_return_auth_error_when_401_fallback_is_exhauste
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert!(content_type.starts_with("text/event-stream"));
|
||||
assert!(body.contains("event: response.failed"));
|
||||
assert!(body.contains("\"code\":\"invalid_api_key\""));
|
||||
assert!(body.contains("All accounts exhausted (1 expired)"));
|
||||
assert!(body.contains("token_revoked"));
|
||||
assert_eq!(account_status.0, "expired");
|
||||
@@ -2062,7 +2065,8 @@ async fn responses_should_return_quota_error_when_402_fallback_is_exhausted() {
|
||||
assert_eq!(status, StatusCode::PAYMENT_REQUIRED);
|
||||
assert!(message.contains("All accounts exhausted (1 quota-exhausted)"));
|
||||
assert!(message.contains("quota reached"));
|
||||
assert_eq!(body["error"]["code"], "upstream_error");
|
||||
assert_eq!(body["error"]["type"], "insufficient_quota");
|
||||
assert_eq!(body["error"]["code"], "insufficient_quota");
|
||||
assert_eq!(account_status.0, "quota_exhausted");
|
||||
}
|
||||
|
||||
@@ -2115,7 +2119,8 @@ async fn responses_should_return_rate_limit_error_when_429_fallback_is_exhausted
|
||||
assert_eq!(status, StatusCode::TOO_MANY_REQUESTS);
|
||||
assert!(message.contains("All accounts exhausted (1 rate-limited)"));
|
||||
assert!(message.contains("rate limited"));
|
||||
assert_eq!(body["error"]["code"], "upstream_error");
|
||||
assert_eq!(body["error"]["type"], "rate_limit_error");
|
||||
assert_eq!(body["error"]["code"], "rate_limit_exceeded");
|
||||
assert_eq!(quota_state.0, 1);
|
||||
assert!(quota_state.1.is_some());
|
||||
let event = latest_response_usage_record(&pool).await;
|
||||
@@ -2272,6 +2277,7 @@ async fn responses_stream_should_return_quota_error_when_402_fallback_is_exhaust
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert!(content_type.starts_with("text/event-stream"));
|
||||
assert!(body.contains("event: response.failed"));
|
||||
assert!(body.contains("\"code\":\"insufficient_quota\""));
|
||||
assert!(body.contains("All accounts exhausted (1 quota-exhausted)"));
|
||||
assert!(body.contains("quota reached"));
|
||||
assert_eq!(account_status.0, "quota_exhausted");
|
||||
|
||||
@@ -1703,7 +1703,7 @@ async fn responses_websocket_without_history_should_mark_expired_after_fallback_
|
||||
|
||||
assert!(body.contains("event: response.failed"));
|
||||
assert!(body.contains("\"type\":\"invalid_request_error\""));
|
||||
assert!(body.contains("\"code\":\"authentication_error\""));
|
||||
assert!(body.contains("\"code\":\"invalid_api_key\""));
|
||||
assert!(body.contains("All accounts exhausted"));
|
||||
assert!(body.contains("token_revoked"));
|
||||
assert_eq!(secondary_status.0, "expired");
|
||||
@@ -1877,8 +1877,8 @@ async fn responses_websocket_without_history_should_return_quota_stream_error_wh
|
||||
|
||||
assert_response_failed_stream(
|
||||
&body,
|
||||
"invalid_request_error",
|
||||
"codex_api_error",
|
||||
"insufficient_quota",
|
||||
"insufficient_quota",
|
||||
&[
|
||||
"All accounts exhausted (1 quota-exhausted)",
|
||||
"quota reached",
|
||||
@@ -1928,7 +1928,7 @@ async fn responses_websocket_without_history_should_return_model_unsupported_str
|
||||
assert_response_failed_stream(
|
||||
&body,
|
||||
"invalid_request_error",
|
||||
"codex_api_error",
|
||||
"model_not_found",
|
||||
&[
|
||||
"No accounts available",
|
||||
"model_not_available",
|
||||
@@ -1973,7 +1973,7 @@ async fn responses_websocket_with_history_should_return_path_block_stream_error_
|
||||
assert_response_failed_stream(
|
||||
&body,
|
||||
"server_error",
|
||||
"codex_api_error",
|
||||
"upstream_error",
|
||||
&["No accounts available", "Cloudflare path-block"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -164,10 +164,13 @@ fn chat_completion_from_codex_sse_should_convert_completed_response() {
|
||||
|
||||
#[test]
|
||||
fn chat_completion_from_codex_sse_should_convert_incomplete_response() {
|
||||
let body = include_str!("../../fixtures/responses/http_sse/chat_delta_incomplete_usage.sse");
|
||||
let body = format!(
|
||||
"{}\n",
|
||||
include_str!("../../fixtures/responses/http_sse/chat_delta_incomplete_usage.sse")
|
||||
);
|
||||
|
||||
let response = codex_proxy_rs::proxy::openai::chat::chat_completion_from_codex_sse(
|
||||
body, "gpt-5.5", false, None,
|
||||
&body, "gpt-5.5", false, None,
|
||||
)
|
||||
.expect("conversion should succeed")
|
||||
.expect("terminal response");
|
||||
@@ -214,12 +217,15 @@ fn chat_completion_stream_translator_should_complete_incomplete_response() {
|
||||
let mut translator = codex_proxy_rs::proxy::openai::chat::ChatCompletionStreamTranslator::new(
|
||||
"gpt-5.5", false, None,
|
||||
);
|
||||
let body = include_str!("../../fixtures/responses/http_sse/chat_delta_incomplete_usage.sse");
|
||||
let body = format!(
|
||||
"{}\n",
|
||||
include_str!("../../fixtures/responses/http_sse/chat_delta_incomplete_usage.sse")
|
||||
);
|
||||
let output = format!(
|
||||
"{}{}",
|
||||
translator.initial_frame(),
|
||||
translator
|
||||
.push_str(body)
|
||||
.push_str(&body)
|
||||
.expect("stream conversion should succeed")
|
||||
);
|
||||
|
||||
|
||||
@@ -84,11 +84,11 @@ response.output_item.done
|
||||
错误体:
|
||||
|
||||
- **quota 耗尽 → HTTP 402**(`dispatch/responses.rs` `http_status_code`、`chat.rs` 同):OpenAI 用 429 + `insufficient_quota`。**判为设计权衡而非 bug** — 402 Payment Required 语义上对应配额耗尽。但若目标是兼容 OpenAI SDK 的限流退避/重试,SDK 的 status→异常映射无 402 分支,会落到通用 `APIStatusError` 而非 `RateLimitError`,退避逻辑失效。取决于产品意图,此处仅标注权衡点。
|
||||
- 聚合错误 type 恒为 `server_error`/`upstream_error`(`errors.rs`):quota/rate 耗尽经账户池聚合路径时 type 未按语义映射为 `insufficient_quota`/`rate_limit_error`,读 `error.type` 判限流的客户端无法识别。
|
||||
- 聚合错误 type/code 语义映射(已修复):quota/rate/auth/model unsupported 经账户池聚合路径时已分别映射为 `insufficient_quota`、`rate_limit_error/rate_limit_exceeded`、`invalid_request_error/invalid_api_key`、`invalid_request_error/model_not_found`。
|
||||
- 错误体缺 `param` 字段(`errors.rs:47-63`);多数 SDK 容忍。
|
||||
- Responses 非流式错误多包一层顶层 `"type":"error"`(`errors.rs:114-141`),偏离 `{"error":{...}}`。
|
||||
- 401(expired/disabled) 聚合路径 status=401 但 type=`server_error`,body 与状态码不自洽。
|
||||
- chat 流式中途失败帧 type=`stream_error` 且无 `code`(`chat.rs:1001`),OpenAI SDK 不识别,等价于坏流。
|
||||
- Responses stream 聚合错误 `response.failed` 已复用 dispatch 语义 code,不再仅按 HTTP status 粗映射;chat 流式中途失败帧仍是 type=`stream_error` 且无 `code`(`chat.rs:1001`),OpenAI SDK 不识别,等价于坏流。
|
||||
|
||||
SSE / chat 转换:
|
||||
|
||||
@@ -120,8 +120,9 @@ Models 端点(结构层面与 OpenAI 契约高度一致):
|
||||
1. **修 `response.incomplete`(🔴,基准硬,已完成)** — 两条 SSE 路径已纳入终止事件集,Chat Completions 已补 `length` finish reason 和 `[DONE]`。
|
||||
2. **chat 工具定义/`tool_choice` 转扁平(🔴,已完成)** — 已参考 sub2api 与 CLIProxyAPI 的 common path,并补充请求转换测试。
|
||||
3. **chat reasoning/custom tool delta 与 `output_item.done` 兜底(🟡,已完成)** — 已补 Chat 流式/非流式转换测试。
|
||||
4. **决策 quota→402 vs 429(🟡,产品权衡)** — 明确是要 OpenAI SDK 兼容还是语义化 HTTP 码。
|
||||
5. 处理聚合错误 type 语义映射、Models 首拉为空/别名详情等中等项。
|
||||
4. **聚合错误 type/code 与 Responses stream code 映射(🟡,已完成)** — Chat/Responses JSON 与 Responses `response.failed` 已共享语义化错误 code;HTTP status 策略未在本项中改变。
|
||||
5. **决策 quota→402 vs 429(🟡,产品权衡)** — 明确是要 OpenAI SDK 兼容还是语义化 HTTP 码。
|
||||
6. 处理 chat stream 中途失败帧、Models 首拉为空/别名详情等中等项。
|
||||
|
||||
## 复现方法(解包基准)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user