mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
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 <noreply@openai.com>
This commit is contained in:
@@ -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 = "<client-api-key>"
|
||||
|
||||
[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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -60,7 +60,7 @@ const visibleFiles = computed(() => [
|
||||
<BaseModal
|
||||
v-model="open"
|
||||
title="使用密钥"
|
||||
description="将下方内容分别保存到显示的 Codex CLI 配置文件"
|
||||
description="将下方内容保存或合并到对应文件,保存后重新启动 Codex"
|
||||
size="lg"
|
||||
>
|
||||
<div class="flex flex-col gap-5">
|
||||
@@ -76,6 +76,10 @@ const visibleFiles = computed(() => [
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="text-cp-sm text-cp-text-secondary">
|
||||
配置含密钥,请勿分享,覆盖前请备份。
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<section
|
||||
v-for="file in visibleFiles"
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
buildCodexConfigFiles,
|
||||
CODEX_DEFAULT_MODEL,
|
||||
CODEX_WEBSOCKET_ENABLED_BY_DEFAULT,
|
||||
} from './codexConfig'
|
||||
} from './codexConfig.ts'
|
||||
|
||||
export interface CodexCcSwitchImportInput {
|
||||
apiKey: string
|
||||
@@ -16,8 +16,7 @@ export function buildCodexCcSwitchImportDeeplink(input: CodexCcSwitchImportInput
|
||||
baseUrl: input.baseUrl,
|
||||
websocketEnabled: CODEX_WEBSOCKET_ENABLED_BY_DEFAULT,
|
||||
})
|
||||
// Carry the complete files shown in the UI; the explicit fields below keep
|
||||
// compatibility with CCSwitch versions that only extract common settings.
|
||||
// 与界面共用原生生图配置;保留 auth 载荷和独立字段,兼容旧版 CCSwitch。
|
||||
const config = encodeBase64(JSON.stringify({
|
||||
auth: configFiles.auth,
|
||||
config: configFiles.configToml,
|
||||
|
||||
@@ -10,22 +10,29 @@ export interface CodexConfigInput {
|
||||
export function buildCodexConfigFiles(input: CodexConfigInput) {
|
||||
const baseUrl = input.baseUrl.replace(/\/+$/, '')
|
||||
const websocketEnabled = input.websocketEnabled ?? CODEX_WEBSOCKET_ENABLED_BY_DEFAULT
|
||||
// 保留 auth.json 载荷,兼容仍依赖该字段的 CCSwitch 导入器。
|
||||
const auth = { OPENAI_API_KEY: input.apiKey }
|
||||
const configToml = `model_provider = "OpenAI"
|
||||
model = "${CODEX_DEFAULT_MODEL}"
|
||||
review_model = "${CODEX_DEFAULT_MODEL}"
|
||||
model_reasoning_effort = "max"
|
||||
service_tier = "default"
|
||||
disable_response_storage = true
|
||||
network_access = "enabled"
|
||||
|
||||
[model_providers.OpenAI]
|
||||
name = "OpenAI"
|
||||
base_url = "${baseUrl}"
|
||||
wire_api = "responses"${websocketEnabled ? '\nsupports_websockets = true' : ''}
|
||||
requires_openai_auth = true
|
||||
base_url = ${JSON.stringify(baseUrl)}
|
||||
wire_api = "responses"
|
||||
supports_websockets = ${websocketEnabled}
|
||||
requires_openai_auth = false
|
||||
# 代理密钥仅用于网关鉴权,真实账号登录状态由服务端管理。
|
||||
experimental_bearer_token = ${JSON.stringify(input.apiKey)}
|
||||
|
||||
[features]${websocketEnabled ? '\nresponses_websockets_v2 = true' : ''}
|
||||
[model_providers.OpenAI.http_headers]
|
||||
# 声明服务端托管认证,让官方客户端启用原生生图;该标记不是密钥。
|
||||
X-OpenAI-Actor-Authorization = "proxy-managed"
|
||||
|
||||
[features]
|
||||
image_generation = true
|
||||
goals = true`
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user