From a404c2e09e967ece52735d6a97eff1b8bff8969d Mon Sep 17 00:00:00 2001 From: Zyy Date: Sun, 6 Sep 2026 15:09:05 +0800 Subject: [PATCH] feat(codex): enable native image generation with managed auth Generate server-managed authentication settings for native image generation while preserving API-key auth.json and CCSwitch compatibility. Remove obsolete client settings and retain provider-level WebSocket control. Keep gateway bearer authentication mandatory, strip client actor authorization headers before forwarding upstream, and cover the boundary with backend regression tests. Document setup and simplify the configuration dialog guidance. Validated with backend tests, live Codex CLI image generation, WebSocket and legacy-auth checks, frontend build, and strict CLI configuration loading. Co-authored-by: Codex --- README.md | 32 +++++++++++++++++++ .../src/openai/responses/request.rs | 2 ++ .../crates/gateway-api/tests/openai/auth.rs | 26 +++++++++++++++ .../gateway-api/tests/openai/responses/mod.rs | 5 +++ .../providers/openai/src/transport/request.rs | 1 + .../openai/tests/transport/headers.rs | 12 ++++++- .../api-keys/components/ApiKeyUseModal.vue | 6 +++- .../views/api-keys/utils/ccswitchImport.ts | 5 ++- .../src/views/api-keys/utils/codexConfig.ts | 19 +++++++---- 9 files changed, 97 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 20dda7a53..36b89412e 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,38 @@ curl http://127.0.0.1:8080/v1/responses \ 完整的客户端与管理端路由、鉴权、请求字段和 mutation 语义见 [接口文档](docs/api.md)。 +### Codex 原生生图 + +在管理端的「使用密钥」中复制配置,或通过 CCSwitch 导入,保存后重启 Codex。 +新配置启用 `features.image_generation = true`,使用以下 Provider 认证方式: + +```toml +[model_providers.OpenAI] +name = "OpenAI" +base_url = "http://127.0.0.1:8080/v1" +wire_api = "responses" +requires_openai_auth = false +# 使用管理端创建的代理密钥,不是 ChatGPT 账号的访问令牌。 +experimental_bearer_token = "" + +[model_providers.OpenAI.http_headers] +# 声明账号认证由服务端托管;此标记本身没有鉴权能力。 +X-OpenAI-Actor-Authorization = "proxy-managed" + +[features] +image_generation = true +``` + +合并到已有配置时,请修改当前 `model_provider` 对应的 Provider 段,不要重复添加同名 TOML 表, +并保留其他个性化设置。配置文件包含代理密钥,请勿公开分享。 +无需更改本地 `auth.json`;真实 ChatGPT 登录状态仍由服务端管理。网关始终校验 Bearer Client Key, +并过滤客户端的 Actor 认证标记,上游身份只来自调度选中的服务端账号。 + +此配置使用官方客户端的服务端托管认证能力,不修改或伪造客户端登录状态。 +需要支持该能力的新版 Codex、支持图片输入及命名空间工具的对话模型,以及具备生图权限的 OpenAI 账号。 +模型会在需要图片时调用原生 `image_gen.imagegen`,图片请求通过本代理的 Images 路由完成。 +原来的 `auth.json` + `requires_openai_auth = true` 配置仍可用于已有请求,但不会因此自动获得原生生图能力。 + ## 运行维护 ```bash diff --git a/backend/crates/gateway-api/src/openai/responses/request.rs b/backend/crates/gateway-api/src/openai/responses/request.rs index 3aeaceeb9..f2aadfd54 100644 --- a/backend/crates/gateway-api/src/openai/responses/request.rs +++ b/backend/crates/gateway-api/src/openai/responses/request.rs @@ -483,6 +483,8 @@ fn passthrough_header_name(name: &str, connection_headers: &[String]) -> bool { // 下游鉴权和账号 cookie 绝不能成为上游账号身份。 | "authorization" | "x-api-key" + // Codex 的服务端托管认证标记只用于客户端能力判断,不代表上游身份。 + | "x-openai-actor-authorization" | "cookie" | "cookie2" | "chatgpt-account-id" diff --git a/backend/crates/gateway-api/tests/openai/auth.rs b/backend/crates/gateway-api/tests/openai/auth.rs index 131fb500e..4bda051b2 100644 --- a/backend/crates/gateway-api/tests/openai/auth.rs +++ b/backend/crates/gateway-api/tests/openai/auth.rs @@ -13,6 +13,32 @@ fn bearer_client_api_key_should_reject_missing_authorization() { ); } +#[test] +fn actor_authorization_marker_should_not_replace_client_api_key() { + let mut headers = HeaderMap::new(); + headers.insert( + "x-openai-actor-authorization", + HeaderValue::from_static("proxy-managed"), + ); + + assert_eq!( + bearer_client_api_key(&headers), + Err(ClientApiKeyAuthError::MissingAuthorization) + ); +} + +#[test] +fn actor_authorization_marker_should_preserve_existing_bearer_authentication() { + let mut headers = HeaderMap::new(); + headers.insert( + "x-openai-actor-authorization", + HeaderValue::from_static("proxy-managed"), + ); + headers.insert(AUTHORIZATION, HeaderValue::from_static("Bearer sk_client")); + + assert_eq!(bearer_client_api_key(&headers), Ok("sk_client")); +} + #[test] fn bearer_client_api_key_should_reject_non_utf8_authorization() { let mut headers = HeaderMap::new(); diff --git a/backend/crates/gateway-api/tests/openai/responses/mod.rs b/backend/crates/gateway-api/tests/openai/responses/mod.rs index 6ee13946c..41f316349 100644 --- a/backend/crates/gateway-api/tests/openai/responses/mod.rs +++ b/backend/crates/gateway-api/tests/openai/responses/mod.rs @@ -281,6 +281,10 @@ fn decoder_should_preserve_ordinary_request_headers_as_opaque_multivalues() { headers.insert("content-length", HeaderValue::from_static("999")); headers.insert("authorization", HeaderValue::from_static("Bearer client")); headers.insert("x-api-key", HeaderValue::from_static("client-key")); + headers.insert( + "x-openai-actor-authorization", + HeaderValue::from_static("proxy-managed"), + ); headers.insert("cookie", HeaderValue::from_static("client=cookie")); headers.insert( "chatgpt-account-id", @@ -348,6 +352,7 @@ fn decoder_should_preserve_ordinary_request_headers_as_opaque_multivalues() { "content-length", "authorization", "x-api-key", + "x-openai-actor-authorization", "cookie", "chatgpt-account-id", "chatgpt-project-id", diff --git a/backend/crates/providers/openai/src/transport/request.rs b/backend/crates/providers/openai/src/transport/request.rs index f3ad5fa0f..873b86bdd 100644 --- a/backend/crates/providers/openai/src/transport/request.rs +++ b/backend/crates/providers/openai/src/transport/request.rs @@ -620,6 +620,7 @@ fn provider_managed_header(name: &str) -> bool { | "x-request-id" | "authorization" | "x-api-key" + | "x-openai-actor-authorization" | "cookie" | "cookie2" | "chatgpt-account-id" diff --git a/backend/crates/providers/openai/tests/transport/headers.rs b/backend/crates/providers/openai/tests/transport/headers.rs index 3a50b68c1..e3fa759bf 100644 --- a/backend/crates/providers/openai/tests/transport/headers.rs +++ b/backend/crates/providers/openai/tests/transport/headers.rs @@ -46,6 +46,10 @@ fn request_with_opaque_headers(use_websocket: bool) -> CodexResponsesRequest { ["x-invalid-base64", "%%%"], ["x-still-valid", STANDARD.encode(b"after-invalid")], ["authorization", STANDARD.encode(b"Bearer client-secret")], + [ + "X-OpenAI-Actor-Authorization", + STANDARD.encode(b"proxy-managed") + ], ["chatgpt-account-id", STANDARD.encode(b"client-account")], [ "x-codex-installation-id", @@ -425,6 +429,7 @@ async fn backend_http_should_restore_opaque_multivalue_header_bytes_and_lease_id ); for dropped in [ "openai-beta", + "x-openai-actor-authorization", "x-oai-attestation", "x-oai-is", "x-oai-is-update", @@ -542,7 +547,12 @@ async fn backend_websocket_should_drop_only_unrepresentable_opaque_header_values values("x-openai-internal-codex-residency"), vec![b"us".to_vec()] ); - for dropped in ["x-oai-attestation", "x-oai-is", "x-oai-is-update"] { + for dropped in [ + "x-openai-actor-authorization", + "x-oai-attestation", + "x-oai-is", + "x-oai-is-update", + ] { assert!(values(dropped).is_empty(), "unexpected {dropped}"); } assert_eq!( diff --git a/frontend/src/views/api-keys/components/ApiKeyUseModal.vue b/frontend/src/views/api-keys/components/ApiKeyUseModal.vue index 04c83e7ca..1f488b1a2 100644 --- a/frontend/src/views/api-keys/components/ApiKeyUseModal.vue +++ b/frontend/src/views/api-keys/components/ApiKeyUseModal.vue @@ -60,7 +60,7 @@ const visibleFiles = computed(() => [
@@ -76,6 +76,10 @@ const visibleFiles = computed(() => [ />
+

+ 配置含密钥,请勿分享,覆盖前请备份。 +

+