Skip to content

Commit 37f97e3

Browse files
committed
Merge branch 'dev'
# Conflicts: # package.json
2 parents 893f202 + 6a6ba06 commit 37f97e3

58 files changed

Lines changed: 3826 additions & 238 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.ko.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ opencodex는 두 가지 동작을 분리해서 유지합니다:
179179
- **알맞은 모델에 위임.** 대시보드나 config에서 최대 5개의 라우팅/네이티브 모델을 Codex 서브에이전트 선택기에 노출해, 복잡한 작업은 reasoning 모델로, 빠른 작업은 저렴한 모델로 보낼 수 있습니다. v2 멀티에이전트 표면(GPT-5.6 Sol/Terra)에서는 프록시가 간결한 위임 가이드를 주입합니다. 선호 서브에이전트 모델·effort(`injectionModel` / `injectionEffort`), 노출된 모델 로스터와 각 모델이 지원하는 effort 사다리, 그리고 크로스모델 `spawn_agent` 호출이 실제로 먹히게 하는 `fork_turns` 규칙까지. 문구를 직접 쓰고 싶다면 `injectionPrompt``{{model}}` / `{{effort}}` / `{{roster}}` 플레이스홀더를 넣으면 됩니다.
180180
- **프리뷰 게이트된 OpenAI rollout에 대비.** GPT-5.6 Sol/Terra/Luna 항목은 upstream 스펙 그대로(Sol/Terra는 `ultra`까지, Luna는 `max`까지; 372k usable context) ChatGPT passthrough, OpenAI API key, OpenRouter route에 준비되어 있습니다.
181181
- **어떤 모델에도 초능력을.** OpenAI가 아닌 모델도 ChatGPT 로그인 위에서 도는 `gpt-5.4-mini` sidecar로 실제 웹 검색과 이미지 이해를 사용합니다.
182+
- **이미지를 네이티브로 생성.** Codex의 독립형 `image_gen` 도구는 생성할 때 `POST /v1/images/generations`, 편집할 때 `POST /v1/images/edits`를 사용합니다. Responses의 hosted `image_generation` 도구와는 별개입니다.
182183
- **무슨 일이 일어나는지 보이게.** 웹 대시보드가 프로바이더, OAuth 상태, 모델 선택, upstream이 보고한 cached/cache-write 토큰 수를 포함한 실시간 요청 로그를 보여줍니다 — 왜 요청이 실패했는지 더는 추측하지 않아도 됩니다.
183184
- **백그라운드 실행.** 시스템 서비스(launchd / systemd / Task Scheduler)로 설치하면 부팅 시 자동 시작되어 신경 쓸 필요가 없습니다.
184185
- **깔끔한 종료, 잔여물 제로.** `ocx stop`(또는 대시보드의 Stop 버튼)은 프록시를 종료하고, 설치된 백그라운드 서비스를 멈추며, Codex를 원래 설정으로 복원합니다. 이후 `codex`는 잔여 설정이나 좀비 프로세스 없이 이전과 똑같이 동작합니다.
@@ -321,7 +322,8 @@ WebSocket 전송은 기본적으로 꺼져 있습니다. Codex가 HTTP/SSE 대
321322
### 원격 접근
322323

323324
기본적으로 opencodex는 `127.0.0.1`(루프백)에 바인딩되며 별도 인증이 필요 없습니다.
324-
`"hostname": "0.0.0.0"`으로 LAN에 노출할 경우, opencodex는 관리 API(`/api/*`)와 데이터 플레인(`/v1/responses`) 모두에 bearer 토큰을 요구합니다:
325+
`"hostname": "0.0.0.0"`으로 LAN에 노출할 경우, opencodex는 관리 API(`/api/*`)와 데이터 플레인
326+
(`/v1/responses`, `/v1/images/generations`, `/v1/images/edits`) 모두에 bearer 토큰을 요구합니다:
325327

326328
```bash
327329
export OPENCODEX_API_AUTH_TOKEN="your-secret-token"
@@ -372,8 +374,9 @@ bun x tsc --noEmit # 타입 체크
372374
```
373375

374376
`bun run dev`는 호환성을 위해 `bun run dev:proxy`의 별칭으로 남아 있습니다. 소스 체크아웃에서 프록시
375-
API는 `/healthz`, `/v1/responses`, `/api/*`를 노출하며, `GET /``bun run build:gui``gui/dist`
376-
생성한 뒤에만 패키징된 대시보드를 서빙합니다. 대시보드를 수정할 때는 프론트엔드를 별도로 실행하세요:
377+
API는 `/healthz`, `/v1/responses`, `POST /v1/images/generations`, `POST /v1/images/edits`, `/api/*`
378+
노출하며, `GET /``bun run build:gui``gui/dist`를 생성한 뒤에만 패키징된 대시보드를 서빙합니다.
379+
대시보드를 수정할 때는 프론트엔드를 별도로 실행하세요:
377380

378381
```bash
379382
bun run dev:gui

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ next Codex session. opencodex keeps two separate behaviors:
184184
- **Delegate to the right model.** Feature up to five routed or native models in Codex's subagent picker from the dashboard or config — route complex tasks to a reasoning model, fast tasks to a cheap one. On the v2 multi-agent surface (GPT-5.6 Sol/Terra) the proxy injects compact delegation guidance: a preferred sub-agent model and effort (`injectionModel` / `injectionEffort`), the featured-model roster with the effort ladder each supports, and the `fork_turns` rules that make cross-model `spawn_agent` calls actually stick. Want your own wording? Set `injectionPrompt` with `{{model}}` / `{{effort}}` / `{{roster}}` placeholders.
185185
- **Prepare for preview-gated OpenAI rollouts.** GPT-5.6 Sol/Terra/Luna entries ship with the exact upstream spec (Sol/Terra reach `ultra`, Luna caps at `max`; 372k usable context) for ChatGPT passthrough, OpenAI API key, and OpenRouter routes when upstream access is available.
186186
- **Give any model superpowers.** Non-OpenAI models get real web search and image understanding via a `gpt-5.4-mini` sidecar over your ChatGPT login.
187+
- **Generate images natively.** Codex's standalone `image_gen` tool uses `POST /v1/images/generations` for generation and `POST /v1/images/edits` for edits; it is separate from the hosted Responses `image_generation` tool.
187188
- **See what's happening.** The web dashboard shows providers, OAuth status, model selection, and a live request log, including cached/cache-write token counts when upstream reports them — no more guessing why a request failed.
188189
- **Runs in the background.** Install as a system service (launchd / systemd / Task Scheduler) and forget about it. The proxy starts on boot and stays out of your way.
189190
- **Clean exit, zero residue.** `ocx stop` (or the dashboard's Stop button) shuts down the proxy, stops the background service if one is installed, and restores Codex to its original configuration. Plain `codex` works exactly as it did before — no leftover config, no orphaned processes.
@@ -341,7 +342,8 @@ WebSocket transport is off by default. Set `"websockets": true` only if you want
341342

342343
By default opencodex binds to `127.0.0.1` (loopback) and requires no extra authentication.
343344
If you set `"hostname": "0.0.0.0"` to expose the proxy on the LAN, opencodex requires a bearer token
344-
to protect both the management API (`/api/*`) and the data-plane (`/v1/responses`):
345+
to protect both the management API (`/api/*`) and the data-plane (`/v1/responses`,
346+
`/v1/images/generations`, and `/v1/images/edits`):
345347

346348
```bash
347349
export OPENCODEX_API_AUTH_TOKEN="your-secret-token"
@@ -392,9 +394,9 @@ bun x tsc --noEmit # typecheck
392394
```
393395

394396
`bun run dev` remains an alias for `bun run dev:proxy` for compatibility. In a source checkout,
395-
the proxy API exposes `/healthz`, `/v1/responses`, and `/api/*`; `GET /` serves the packaged
396-
dashboard only after `bun run build:gui` has produced `gui/dist`. While hacking on the dashboard,
397-
run the frontend separately:
397+
the proxy API exposes `/healthz`, `/v1/responses`, `POST /v1/images/generations`,
398+
`POST /v1/images/edits`, and `/api/*`; `GET /` serves the packaged dashboard only after
399+
`bun run build:gui` has produced `gui/dist`. While hacking on the dashboard, run the frontend separately:
398400

399401
```bash
400402
bun run dev:gui

README.zh-CN.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ npm install -g @bitkyc08/opencodex # 不要加 --ignore-scripts、--omit=optio
102102
- **委派给合适的模型。** 在仪表盘或 config 中把最多 5 个路由/原生模型放进 Codex 的 subagent 选择器 —— 复杂任务交给 reasoning 模型,快速任务交给便宜模型。在 v2 多智能体表面(GPT-5.6 Sol/Terra)上,代理会注入精简的委派指引:首选子智能体模型与 effort(`injectionModel` / `injectionEffort`)、featured 模型清单及各自支持的 effort 阶梯,以及让跨模型 `spawn_agent` 调用真正生效的 `fork_turns` 规则。想自定义文案,可在 `injectionPrompt` 中使用 `{{model}}` / `{{effort}}` / `{{roster}}` 占位符。
103103
- **为 preview-gated OpenAI rollout 做好准备。** GPT-5.6 Sol/Terra/Luna 条目采用与 upstream 完全一致的规格(Sol/Terra 到 `ultra`,Luna 到 `max`;372k 可用上下文),覆盖 ChatGPT passthrough、OpenAI API key 和 OpenRouter 路由。
104104
- **给任意模型超能力。** 非 OpenAI 模型也能通过你的 ChatGPT 登录上运行的 `gpt-5.4-mini` sidecar 获得真正的网页搜索和图片理解。
105+
- **原生生成图片。** Codex 的独立 `image_gen` 工具通过 `POST /v1/images/generations` 生成图片、通过 `POST /v1/images/edits` 编辑图片;它独立于 hosted Responses 的 `image_generation` 工具。
105106
- **看清正在发生什么。** Web 仪表盘展示 provider、OAuth 状态、模型选择和实时请求日志;当上游返回时,也会包含 cached/cache-write token 计数 —— 不必再猜测请求为何失败。
106107
- **后台运行。** 安装为系统服务(launchd / systemd / Task Scheduler)后开机自启,无需操心。
107108
- **干净退出,零残留。** `ocx stop`(或仪表盘的 Stop 按钮)会关闭代理、停止已安装的后台服务,并将 Codex 恢复为原始配置。之后 `codex` 就像从未安装过 opencodex 一样工作 —— 无残留配置,无僵尸进程。
@@ -303,7 +304,7 @@ WebSocket 传输默认关闭。只有当你希望 Codex 使用 Responses WebSock
303304

304305
默认情况下 opencodex 绑定到 `127.0.0.1`(回环)且无需额外认证。
305306
如果你设置 `"hostname": "0.0.0.0"` 把代理暴露到局域网,opencodex 会要求一个 bearer token 来同时保护管理
306-
API(`/api/*`)和数据平面(`/v1/responses`):
307+
API(`/api/*`)和数据平面(`/v1/responses``/v1/images/generations``/v1/images/edits`):
307308

308309
```bash
309310
export OPENCODEX_API_AUTH_TOKEN="your-secret-token"
@@ -350,8 +351,8 @@ bun x tsc --noEmit # 类型检查
350351
```
351352

352353
`bun run dev` 作为 `bun run dev:proxy` 的别名保留以兼容旧用法。在源码检出中,代理 API 暴露 `/healthz`
353-
`/v1/responses``/api/*`;只有在 `bun run build:gui` 生成 `gui/dist` 之后,`GET /` 才会提供打包后的仪表盘。
354-
开发前端时请单独运行:
354+
`/v1/responses``POST /v1/images/generations``POST /v1/images/edits``/api/*`;只有在
355+
`bun run build:gui` 生成 `gui/dist` 之后,`GET /` 才会提供打包后的仪表盘。开发前端时请单独运行:
355356

356357
```bash
357358
bun run dev:gui
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# 260710 — /v1/images relay for codex's built-in image_gen (issue #83)
2+
3+
## Problem
4+
5+
GitHub issue #83: the built-in `image_gen` tool fails instantly with
6+
`http 404 Not Found: {"error":{"message":"Unknown endpoint: POST /v1/images/generations",...}}`.
7+
8+
The 404 is ours: codex-rs's standalone image-generation extension executes CLIENT-SIDE —
9+
it POSTs `{base_url}/images/generations` (edits variant: `/images/edits`) with the same
10+
ChatGPT bearer auth it uses for chat (`ext/image-generation`, request body
11+
`{prompt, background, model:"gpt-image-2", quality, size}`, expected response
12+
`{created, data:[{b64_json}]}`, both strict serde). Under Design B injection `base_url`
13+
IS the proxy, and the request died on the `/v1/*` JSON-404 guard (previously even
14+
test-pinned as an intentional 404 in server-auth.test.ts).
15+
16+
Why now: codex 0.144.0 graduated `image_generation` from the disabled-by-default
17+
`imagegenext` experiment to Stage::Stable / default-enabled, so ChatGPT-signed-in users
18+
started hitting the tool organically. The hosted `image_generation` tools[] entry on
19+
`/v1/responses` was never affected (passthrough already forwards it); only the
20+
extension's direct REST call 404'd.
21+
22+
## Fix
23+
24+
New relay route `POST /v1/images/{generations,edits}` (src/server/images.ts), inserted
25+
before the /v1/* guard with the standard data-plane preamble (drain 503, requireApiAuth,
26+
origin check, withCors, request log with `client_cancel` meta on 499).
27+
28+
Upstream selection (`findImagesUpstreams` collects BOTH candidates; selection is
29+
per-request):
30+
31+
1. **ChatGPT forward provider** — preferred, same precedence as the vision/web-search
32+
sidecars; it is the backend codex itself would have called absent the base_url
33+
override, so request/response bodies relay verbatim (no mapping). Auth via
34+
`resolveCodexAuthContext`/`headersForCodexAuthContext`: forwarded caller bearer, or
35+
the routed multi-account pool token. Guards:
36+
- `startServer` auto-upserts a `chatgpt` forward entry into every config, so the
37+
forward candidate is only used when the resolved headers actually carry an
38+
authorization value — otherwise an unauthenticated request would bounce off
39+
chatgpt.com as an opaque `{"detail":"Unauthorized"}`.
40+
- A bearer equal to the proxy's own admission secret (non-loopback binds accept
41+
`Authorization: Bearer <OPENCODEX_API_AUTH_TOKEN>`) is stripped before selection —
42+
the proxy secret must never be relayed to chatgpt.com
43+
(`isProxyAdmissionSecret`, extracted from `hasValidApiAuth`).
44+
- Forward-auth FAILURES (pool cooldown 429, reauth 401, affinity 409) are captured,
45+
not returned: a configured keyed provider still serves the request, and the
46+
captured error only surfaces when no keyed candidate exists. Without this, "all
47+
pool accounts cooling down" would 429 image_gen while api.openai.com sat idle.
48+
- Pool upstream outcomes are recorded via `sidecarOutcomeRecorder` (status /
49+
timeout / connect_error), keeping rotation failover signals alive.
50+
2. **Keyed openai-responses provider** (e.g. api.openai.com) — fallback when no
51+
relayable ChatGPT auth; its `/v1/images/*` is the real platform Images API. URL is
52+
normalized like the adapter (`baseUrl.replace(/\/v1\/?$/,"")` + `/v1/images/…`), so
53+
baseUrl with or without `/v1` both work; forward URLs stay bare
54+
(`${baseUrl}/images/…`, the ChatGPT-backend convention).
55+
3. **Neither** — honest 400 (`invalid_request_error`) naming the fix
56+
(`codex features disable image_generation` or add an OpenAI provider). 400, not 5xx:
57+
codex retries every 5xx up to 5 total attempts and this is a permanent config state.
58+
59+
Relay details: `readJsonRequestBody` (zstd/gzip decode + 256MB cap) → JSON re-serialize →
60+
`fetchWithResetRetry` (stale keep-alive resets) with a `config.images.timeoutMs`
61+
(default 300s) timeout linked to the client abort; response buffered (single JSON
62+
document, few MB) and passed through status+content-type+body verbatim so upstream
63+
plan-gating errors stay legible. Client cancel returns 499 (`client_closed_request`),
64+
never a fake 502; genuine timeout returns 504 (retriable by codex, acceptable for a
65+
transient hang).
66+
67+
Deliberately NOT done:
68+
69+
- No auto-injection of `[features] image_generation = false` — it would remove a
70+
capability the relay makes work, and openai/codex#21952 suggests app-server ignores
71+
the flag anyway. Documented as a user opt-out instead (docs-site codex-integration,
72+
en/ko/zh-cn).
73+
- No image generation via routed providers (Gemini image models etc.) — the adapter
74+
event pipeline has no image-output event; separate feature, not this bug fix.
75+
76+
## Tests
77+
78+
tests/server-images.test.ts (16): forward relay (auth + path + body), edits path,
79+
pool-token override (caller bearer must not leak), zstd body decode, keyed fallback +
80+
`/v1` URL normalization (with and without suffix), unauthenticated-request gate (keyed
81+
fallback / 401), forward-auth-failure fallback (keyed / surfaced 401), no-upstream 400,
82+
upstream error passthrough, 504 timeout via `config.images.timeoutMs`, GET falls to 404
83+
guard, non-loopback auth/origin, admission-secret never relayed.
84+
server-auth.test.ts's 404-guard list swaps `/v1/images/generations` for
85+
`/v1/realtime/sessions`.

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,24 @@ fast_mode = true
2323
```
2424

2525
The proxy listens on port `10100` by default and serves `POST /v1/responses`,
26-
`POST /v1/responses/compact`, `GET /v1/models`, `GET /healthz`, and the `/api/*` management surface.
26+
`POST /v1/responses/compact`, `POST /v1/images/generations`, `POST /v1/images/edits`,
27+
`GET /v1/models`, `GET /healthz`, and the `/api/*` management surface.
28+
29+
### Built-in image generation (`image_gen`)
30+
31+
Codex's built-in `image_gen` tool does not go through `/v1/responses` — the codex-rs extension
32+
POSTs `{base_url}/images/generations` (or `/images/edits` when reference images are attached)
33+
directly, with the same ChatGPT bearer auth it uses for chat. Because the injected `base_url`
34+
points at opencodex, the proxy relays those calls to the OpenAI upstream:
35+
36+
- **ChatGPT login (default):** the request is forwarded to `chatgpt.com/backend-api/codex` with
37+
the caller's OAuth token (or the routed multi-account pool token). No API key is needed.
38+
- **OpenAI API-key provider:** if no ChatGPT auth is available on the request, the relay falls
39+
back to a configured `openai-responses` API-key provider (e.g. `api.openai.com`).
40+
- **Neither:** the proxy returns a clear error instead of a generic 404. Routed providers
41+
(Cursor, Gemini, Kiro, …) cannot serve image generation; if you don't want the tool offered at
42+
all, disable it in Codex with `codex features disable image_generation`
43+
(`[features] image_generation = false` in `config.toml`).
2744

2845
For a non-loopback `hostname`, Codex must send the generated API auth header. The injector therefore
2946
uses a dedicated provider instead:

docs-site/src/content/docs/guides/sidecars.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ When Codex requests hosted `web_search` for a non-passthrough routed model, open
2828
(default 3), then removes the search tool and forces a final answer. Real client tools such as
2929
`apply_patch` or shell finalize the turn so those calls reach Codex.
3030

31+
Every routed-model iteration requests upstream `stream: true`, but opencodex fully buffers semantic
32+
events internally before deciding whether to search or return the final answer. Only the first
33+
iteration's final headers/status and 429 key rotations are acquired eagerly. Thus synthetic search
34+
calls and preliminary output are never exposed as client-visible model output.
35+
3136
The injected result is wrapped in an untrusted-data boundary, length-capped, and de-duplicated by
3237
source URL. In structured-output turns (`json_schema` / `json_object`) it is handed over as compact
3338
JSON instead of prose. For text-only routed models, the search model is also told to describe
@@ -40,6 +45,7 @@ relevant images in words and include their source URLs.
4045
"model": "gpt-5.6-luna",
4146
"reasoning": "low",
4247
"maxSearchesPerTurn": 3,
48+
"routedModelStallTimeoutMs": 200000,
4349
"timeoutMs": 200000
4450
}
4551
}
@@ -49,6 +55,16 @@ relevant images in words and include their source URLs.
4955
search is returned to the routed model as a bounded error result, allowing it to answer from the
5056
context it already has.
5157

58+
Four separate clocks apply. `stallTimeoutSec` is the base bridge event-stall budget.
59+
`connectTimeoutMs` (default `200000`) covers only DNS/TCP/TLS and final response headers.
60+
Config-file-only `webSearchSidecar.routedModelStallTimeoutMs` (default `200000`, integer
61+
`1..2147483647`) bounds continuous raw response-byte inactivity for each routed-model iteration and
62+
resets on every non-empty byte. `webSearchSidecar.timeoutMs` separately bounds one hosted search
63+
request. The effective bridge watchdog is
64+
`max(base stall, connect timeout, routed-model stall, sidecar timeout) + 30 seconds`. The routed
65+
stall is not a total generation timeout. Failures before SSE starts return non-2xx JSON; generation
66+
failures after response headers have started are delivered as `response.failed` SSE.
67+
5268
## Vision sidecar
5369

5470
When the routed model is listed in its provider's `noVisionModels` and a request carries an image,

0 commit comments

Comments
 (0)