Skip to content

Commit 14ae6fe

Browse files
authored
fix(images): support custom relay providers
Merge PR #551 after current-head verification.\n\nEvidence:\n- head f51b64a is based on dev 261abb7\n- bun test tests/server-images.test.ts: 28 pass / 0 fail\n- bun x tsc --noEmit: pass\n- bun run privacy:scan: pass\n- git diff --check origin/dev...HEAD: pass\n- pre-push equivalent gate on local same-behavior patch: root tests 5066 pass / 0 fail, GUI lint pass, React Doctor no issues\n\nThe stale CHANGES_REQUESTED review named blockers that are now fixed on the current head.
1 parent 261abb7 commit 14ae6fe

12 files changed

Lines changed: 358 additions & 19 deletions

File tree

docs-site/src/content/docs/guides/codex-integration.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,39 @@ points at opencodex, the proxy relays those calls to the OpenAI upstream:
4242
caller OAuth bearer. The configured mode applies consistently to the image request.
4343
- **OpenAI API-key provider:** it is used only when no forward candidate owns an authentication
4444
failure. A broken/expired Pool credential is never hidden behind separately billed API usage.
45+
- **Explicit custom provider:** set `images.provider` to the id of a custom API-key
46+
`openai-responses` provider whose endpoint implements the OpenAI Images API. Explicit selection
47+
fails closed and never falls back to a different paid upstream. Registry-managed provider ids
48+
are not accepted here; omit `images.provider` to use the built-in OpenAI tiers.
4549
- **Neither:** the proxy returns a clear error instead of a generic 404. Routed providers
4650
(Cursor, Gemini, Kiro, …) cannot serve image generation; if you don't want the tool offered at
4751
all, disable it in Codex with `codex features disable image_generation`
4852
(`[features] image_generation = false` in `config.toml`).
4953

54+
For an OpenAI-compatible custom gateway, configure a dedicated provider and select it only for
55+
standalone Images requests:
56+
57+
```json
58+
{
59+
"providers": {
60+
"custom-images": {
61+
"adapter": "openai-responses",
62+
"baseUrl": "https://gateway.example.com/v1",
63+
"authMode": "key",
64+
"apiKey": "${IMAGE_GATEWAY_API_KEY}"
65+
}
66+
},
67+
"images": {
68+
"provider": "custom-images",
69+
"timeoutMs": 300000
70+
}
71+
}
72+
```
73+
74+
The custom endpoint must accept `POST /v1/images/generations` and `/v1/images/edits` and return the
75+
OpenAI Images response shape expected by Codex. The provider's configured key replaces any caller
76+
bearer before the upstream request.
77+
5078
For a non-loopback `hostname`, Codex must send the generated API auth header. The injector therefore
5179
uses a dedicated provider instead:
5280

docs-site/src/content/docs/ja/guides/codex-integration.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,12 @@ ChatGPT bearer 認証で直接 POST します。注入された `base_url` が o
4141
bearer を使います。設定されたモードは画像リクエストにも同じく適用されます。
4242
- **OpenAI API キー:** forward 候補が認証失敗を所有しないときのみ使います。壊れた Pool 認証を
4343
別課金 API 使用で隠しません。
44+
- **明示的なカスタムプロバイダー:** `images.provider` に、OpenAI Images API を実装するカスタムの
45+
API キー方式 `openai-responses` プロバイダーを指定できます。明示的な選択が失敗しても、別の
46+
有料上流へフォールバックしません。組み込みプロバイダー id には使わず、既定の OpenAI 経路を
47+
使う場合は省略してください。
4448
- **両方なし:** 曖昧な 404 の代わりに明確なエラーを返します。ルーティングされる他のプロバイダー(Cursor、
45-
Gemini、Kiro など)は画像生成を提供できません。ツール自体をオフにしたい場合は Codex で
49+
Gemini、Kiro など)は既定では画像生成を提供できません。ツール自体をオフにしたい場合は Codex で
4650
`codex features disable image_generation`(`config.toml``[features] image_generation = false`)を
4751
使ってください。
4852

docs-site/src/content/docs/ko/guides/codex-integration.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@ ChatGPT bearer 인증으로 직접 POST합니다. 주입된 `base_url`이 openco
4141
bearer를 사용합니다. 설정된 모드는 이미지 요청에도 동일하게 적용됩니다.
4242
- **OpenAI API key:** forward 후보가 인증 실패를 소유하지 않을 때만 사용합니다. 깨진 Pool 인증을
4343
별도 과금 API 사용으로 숨기지 않습니다.
44+
- **명시적 커스텀 프로바이더:** `images.provider`에 OpenAI Images API를 구현한 커스텀 API-key
45+
`openai-responses` 프로바이더를 지정할 수 있습니다. 명시적 선택이 실패해도 다른 유료 업스트림으로
46+
fallback하지 않습니다. 내장 프로바이더 id에는 사용하지 말고, 기본 OpenAI 경로를 쓰려면 생략하세요.
4447
- **둘 다 없음:** 모호한 404 대신 명확한 오류를 반환합니다. 라우팅되는 다른 프로바이더(Cursor,
45-
Gemini, Kiro 등)는 이미지 생성을 제공할 수 없습니다. 도구 자체를 끄고 싶다면 Codex에서
48+
Gemini, Kiro 등)는 기본적으로 이미지 생성을 제공할 수 없습니다. 도구 자체를 끄고 싶다면 Codex에서
4649
`codex features disable image_generation`(`config.toml``[features] image_generation = false`)을
4750
사용하세요.
4851

docs-site/src/content/docs/reference/configuration.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids.
6262
| `cacheRetention?` | `"none" \| "short" \| "long"` | `"short"` | Anthropic prompt-cache policy: disabled, 5-minute ephemeral, or 1-hour extended. |
6363
| `webSearchSidecar?` | `OcxWebSearchSidecarConfig` | on | Web-search sidecar options (see below). |
6464
| `visionSidecar?` | `OcxVisionSidecarConfig` | on | Vision sidecar options (see below). |
65+
| `images?` | `OcxImagesConfig` | automatic OpenAI selection | Standalone Images relay options for Codex's built-in `image_gen` tool (see below). |
6566
| `tokenGuardian?` | `OcxTokenGuardianConfig` | off | Optional proactive OAuth refresh and Codex-account warmup policy; fields are listed below. |
6667
| `corsAllowOrigins?` | `string[]` | `[]` | Additional exact origins allowed by CORS. Loopback origins are always allowed. |
6768

@@ -419,6 +420,17 @@ with those explicit additions, or set it to `false` to expose only `models`.
419420

420421
## Sidecars
421422

423+
### `images` (`OcxImagesConfig`)
424+
425+
| Field | Type | Default | Meaning |
426+
| --- | --- | --- | --- |
427+
| `provider?` | `string` | automatic OpenAI selection | Explicit custom API-key `openai-responses` provider for `/v1/images/generations` and `/v1/images/edits`. Registry-managed ids are rejected; omit this field to use the built-in ChatGPT/OpenAI fallback. |
428+
| `timeoutMs?` | `number` | `300000` | Whole-request upstream timeout for one standalone Images request. |
429+
430+
Explicit provider selection fails closed when the provider is missing, disabled, incompatible, or
431+
has no usable key. It never falls back to another paid upstream. The custom endpoint must implement
432+
the OpenAI Images API paths and response shape expected by Codex.
433+
422434
### `webSearchSidecar` (`OcxWebSearchSidecarConfig`)
423435

424436
| Field | Type | Default | Meaning |

docs-site/src/content/docs/ru/guides/codex-integration.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@ fast_mode = true
4646
- **Провайдер с API-ключом OpenAI:** используется только тогда, когда ни один forward-кандидат
4747
не «владеет» сбоем аутентификации. Сломанные или истёкшие учётные данные Pool никогда не
4848
маскируются отдельно оплачиваемым использованием API.
49+
- **Явный пользовательский провайдер:** в `images.provider` можно указать пользовательский
50+
API-key-провайдер `openai-responses`, реализующий OpenAI Images API. Ошибка явного выбора не
51+
приводит к fallback на другую платную вышестоящую сторону. Встроенные id провайдеров здесь не
52+
используются; чтобы сохранить стандартный путь OpenAI, опустите это поле.
4953
- **Ни того, ни другого:** прокси возвращает понятную ошибку вместо безликого 404.
50-
Маршрутизируемые провайдеры (Cursor, Gemini, Kiro, …) не могут обслуживать генерацию
54+
Маршрутизируемые провайдеры (Cursor, Gemini, Kiro, …) по умолчанию не могут обслуживать генерацию
5155
изображений; если вы вообще не хотите предлагать этот инструмент, отключите его в Codex
5256
командой `codex features disable image_generation`
5357
(`[features] image_generation = false` в `config.toml`).

docs-site/src/content/docs/zh-cn/guides/codex-integration.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ OpenAI 上游:
4040
OAuth bearer。图像请求遵循同一模式。
4141
- **OpenAI API key:** 仅当 forward 候选没有拥有认证失败时使用。不会用单独计费的 API 调用掩盖
4242
损坏或过期的 Pool 凭证。
43+
- **显式自定义 provider:** 可将 `images.provider` 设为一个自定义 API-key
44+
`openai-responses` provider;该 endpoint 必须实现 OpenAI Images API。显式选择失败时不会
45+
fallback 到其他付费上游。内置 provider id 不适用于此字段;省略它即可使用默认 OpenAI 路径。
4346
- **两者都没有:** proxy 返回明确的错误而不是含糊的 404。其他路由提供商(Cursor、Gemini、
44-
Kiro 等)无法提供图像生成;如果想完全关闭该工具,可在 Codex 中执行
47+
Kiro 等)默认无法提供图像生成;如果想完全关闭该工具,可在 Codex 中执行
4548
`codex features disable image_generation`(即 `config.toml`
4649
`[features] image_generation = false`)。
4750

src/providers/openai-sidecar.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
OPENAI_API_PROVIDER_ID,
1616
OPENAI_CODEX_PROVIDER_ID,
1717
} from "./openai-tiers";
18-
import { providerCodexAccountMode } from "./registry";
18+
import { getProviderRegistryEntry, providerCodexAccountMode } from "./registry";
1919

2020
export interface OpenAiForwardSidecarCandidate {
2121
providerName: typeof OPENAI_CODEX_PROVIDER_ID;
@@ -32,10 +32,11 @@ export interface ResolvedOpenAiForwardSidecar extends OpenAiForwardSidecarCandid
3232
export interface OpenAiImagesProviderSelection {
3333
forwardCandidates: OpenAiForwardSidecarCandidate[];
3434
keyed?: {
35-
providerName: typeof OPENAI_API_PROVIDER_ID;
35+
providerName: string;
3636
provider: OcxProviderConfig;
3737
apiKey: string;
3838
};
39+
error?: string;
3940
}
4041

4142
export function listOpenAiForwardSidecarCandidates(config: OcxConfig): OpenAiForwardSidecarCandidate[] {
@@ -126,3 +127,46 @@ export function selectOpenAiImagesProvider(config: OcxConfig): OpenAiImagesProvi
126127
}
127128
return selection;
128129
}
130+
131+
/** Resolve an explicit custom Images provider, otherwise preserve the existing OpenAI fallback. */
132+
export function selectImagesProvider(config: OcxConfig): OpenAiImagesProviderSelection {
133+
const configuredProvider = config.images?.provider;
134+
if (configuredProvider === undefined) return selectOpenAiImagesProvider(config);
135+
if (typeof configuredProvider !== "string" || !configuredProvider.trim()) {
136+
return { forwardCandidates: [], error: "images.provider must be a nonblank provider name" };
137+
}
138+
const providerName = configuredProvider.trim();
139+
140+
if (getProviderRegistryEntry(providerName)) {
141+
return {
142+
forwardCandidates: [],
143+
error: `images.provider "${providerName}" must name a custom provider; omit it to use built-in OpenAI tiers`,
144+
};
145+
}
146+
147+
const provider = Object.prototype.hasOwnProperty.call(config.providers, providerName)
148+
? config.providers[providerName]
149+
: undefined;
150+
if (!provider) {
151+
return { forwardCandidates: [], error: `images.provider "${providerName}" is not configured` };
152+
}
153+
if (provider.disabled === true) {
154+
return { forwardCandidates: [], error: `images.provider "${providerName}" is disabled` };
155+
}
156+
if (provider.adapter !== "openai-responses" || (provider.authMode !== undefined && provider.authMode !== "key")) {
157+
return {
158+
forwardCandidates: [],
159+
error: `images.provider "${providerName}" must be an API-key openai-responses provider`,
160+
};
161+
}
162+
163+
const apiKey = resolveEnvValue(provider.apiKey)?.trim();
164+
if (!apiKey) {
165+
return { forwardCandidates: [], error: `images.provider "${providerName}" has no usable API key` };
166+
}
167+
168+
return {
169+
forwardCandidates: [],
170+
keyed: { providerName, provider, apiKey },
171+
};
172+
}

src/server/images.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
* without a route the tool died on the /v1/* JSON-404 guard. Only an OpenAI-family upstream
88
* can serve these endpoints — routed providers (Cursor, Kiro, Gemini, …) have no image
99
* generation surface — so the handler relays the body verbatim to the ChatGPT forward
10-
* provider (or an OpenAI API-key provider) and passes the response through untouched:
10+
* provider, an OpenAI API-key provider, or an explicitly selected compatible custom provider and
11+
* passes the response through untouched:
1112
* codex's images client parses `{created, data:[{b64_json}]}` strictly and Debug-prints
1213
* error bodies into the model-visible failure, so upstream errors must stay legible.
1314
*/
@@ -23,7 +24,7 @@ import { formatCodexProviderForLog } from "../codex/routing";
2324
import { signalWithTimeout } from "../lib/abort";
2425
import { sidecarEnter } from "../lib/sidecar-tracker";
2526
import type { OcxConfig } from "../types";
26-
import { resolveFirstUsableOpenAiSidecar, selectOpenAiImagesProvider } from "../providers/openai-sidecar";
27+
import { resolveFirstUsableOpenAiSidecar, selectImagesProvider } from "../providers/openai-sidecar";
2728
import { readJsonRequestBody } from "./request-decompress";
2829
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
2930
import type { RequestLogContext } from "./request-log";
@@ -47,10 +48,17 @@ export async function handleImages(
4748
endpoint: ImagesEndpoint,
4849
logCtx: RequestLogContext,
4950
): Promise<Response> {
50-
try { validateForwardAdmissionCredential(req.headers, config); }
51-
catch (err) {
52-
if (err instanceof ForwardAdmissionCredentialError) return formatErrorResponse(401, "authentication_error", err.message);
53-
throw err;
51+
const candidates = selectImagesProvider(config);
52+
if (candidates.error) {
53+
return formatErrorResponse(400, "invalid_request_error", candidates.error);
54+
}
55+
const explicitKeyedProvider = config.images?.provider !== undefined && candidates.keyed !== undefined;
56+
if (!explicitKeyedProvider) {
57+
try { validateForwardAdmissionCredential(req.headers, config); }
58+
catch (err) {
59+
if (err instanceof ForwardAdmissionCredentialError) return formatErrorResponse(401, "authentication_error", err.message);
60+
throw err;
61+
}
5462
}
5563
let body: unknown;
5664
try {
@@ -61,7 +69,6 @@ export async function handleImages(
6169
const model = (body as { model?: unknown } | null)?.model;
6270
if (typeof model === "string" && model) logCtx.model = model;
6371

64-
const candidates = selectOpenAiImagesProvider(config);
6572
if (candidates.forwardCandidates.length === 0 && !candidates.keyed) {
6673
// 400, not 5xx: codex retries every 5xx up to 5 total attempts, and this is a permanent
6774
// configuration state that must surface on the first attempt.

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,8 @@ export interface OcxTokenGuardianConfig {
712712
}
713713

714714
export interface OcxImagesConfig {
715+
/** Optional custom API-key provider for /v1/images relays. Built-in OpenAI tiers remain automatic. */
716+
provider?: string;
715717
/** Upstream timeout (ms) for one /v1/images relay. Default 300000 — generation is slow. */
716718
timeoutMs?: number;
717719
}

structure/01_runtime.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
| `src/lib/bun-runtime.ts` | Bundled-Bun resolution: `isRealBunBinary()` (size gate vs the ~450-byte placeholder stub), `bundledBunPath()`, `durableBunPath()` (path baked into service/shim artifacts). |
99
| `src/cli/index.ts` | `ocx` / `opencodex` CLI: init, start, stop, restore/eject, sync, status, login/logout, gui, service, update. After help/version early exits, ordinary commands run the bounded best-effort Codex-shim auto-restore policy before dispatch. Keeps the `#!/usr/bin/env bun` shebang for from-source dev (`bun run src/cli/index.ts`). |
1010
| `src/server/index.ts` | Bun server entrypoint: `startServer`, `/v1/responses` HTTP + WebSocket routing, exact `POST /v1/images/generations` and `POST /v1/images/edits` routing, `/v1/models`, `/v1/*` JSON 404 guard, GUI fallback, and facade re-exports for split server modules. |
11-
| `src/server/images.ts` | Standalone Images data plane: forward-provider selection, Codex account affinity, bounded opaque request relay, single-attempt upstream fetch, pool health recording, and safe response/cancellation relay. |
11+
| `src/server/images.ts` | Standalone Images data plane: default OpenAI or explicit custom-provider selection, Codex account affinity, bounded opaque request relay, single-attempt upstream fetch, pool health recording, and safe response/cancellation relay. |
1212
| `src/config.ts` | `~/.opencodex/config.json`, defaults, PID path, env-value resolution, `websocketsEnabled()`. |
1313
| `src/router.ts` | Provider/model selection before adapter dispatch. |
1414
| `src/types.ts` | Shared config, parsed request, adapter, and event types. |

0 commit comments

Comments
 (0)