codex-grok-bridge 1.7.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/README.md +129 -34
- package/package.json +1 -1
- package/src/bridge.mjs +135 -22
- package/src/errors.mjs +8 -1
- package/src/imagine.mjs +150 -0
- package/src/proxy.mjs +235 -36
- package/src/runtime.mjs +5 -8
- package/src/tools.mjs +256 -27
- package/src/videogen.mjs +196 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 1.7.2 — 2026-09-30
|
|
4
|
+
|
|
5
|
+
- Readable compaction summaries are passed through as user messages. A compaction item that only carries encrypted or internal fields is still dropped.
|
|
6
|
+
- A Grok thread can generate a video without an API key. The bridge declares `grok_bridge_generate_video` on the upstream request and, when the model calls it, posts to `https://api.x.ai/v1/videos/generations` with the grok login bearer, polls `GET /videos/{request_id}`, and returns the video URL to the model as the tool result. A refusal from that API stays a tool error and does not fail the turn.
|
|
7
|
+
- Codex input items Grok does not accept (`local_shell_call`, `web_search_call`, `tool_search_call`, `tool_search_output`, `additional_tools`, `image_generation_call`, and unmapped custom tool items) keep their readable text as user `input_text` messages, the same shape as a compaction summary. That text includes shell commands, search queries, tool-search text, an image `revised_prompt`, and custom tool text. `encrypted_content`, `encrypted_function_args`, `internal_*` fields, and image-byte `result` values stay dropped.
|
|
8
|
+
- `POST /v1/images/generations` and `POST /v1/images/edits` are forwarded to the Imagine API with the grok login bearer. Codex's `gpt-image-2` body is rewritten to `grok-imagine-image-quality` and `b64_json`. OpenAI `file_id` edits are refused. No `XAI_API_KEY`.
|
|
9
|
+
- Codex `web_search` is kept as one server-side `{type:"web_search"}` tool. `web_search_call` results are passed through to Codex.
|
|
10
|
+
- Voice WebRTC (`POST /v1/realtime/calls`) and Codex cloud tasks are not in this package.
|
|
11
|
+
|
|
12
|
+
## 1.7.1 — 2026-09-30
|
|
13
|
+
|
|
14
|
+
- The bridge holds the upstream reply and sends it to Codex only after `response.completed`. A reset before that (`ECONNRESET`, `EPIPE`, `UND_ERR_SOCKET`, or a close before the reply finishes) is retried up to two more times, and only if Codex has not been sent a byte. This provider's Codex `request_max_retries` and `stream_max_retries` are 0. A retry sends the prompt again, so that attempt's input tokens can be billed again.
|
|
15
|
+
- When `grok --version` fails or does not report a version, the client version is `unknown` instead of the stale `1.0.24`.
|
|
4
16
|
|
|
5
17
|
## 1.7.0 — 2026-09-29
|
|
6
18
|
|
package/README.md
CHANGED
|
@@ -30,13 +30,45 @@ The bridge does **not** execute Grok-native tools. It translates Codex tools int
|
|
|
30
30
|
function calling, streams the upstream Responses events, and rewrites names back
|
|
31
31
|
so Codex still recognizes them.
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Eighteen files under `src/`. Zero runtime dependencies. Node.js ≥ 22.
|
|
34
34
|
|
|
35
35
|
This is not a second-opinion review product and not a Grok-native search
|
|
36
36
|
product. The picker shows `grok-4.7` (Grok 4.7 / xAI) and keeps `grok-4.6`.
|
|
37
37
|
Any other `grok-*` id uses the same route. `GROK_BRIDGE_MODELS` adds further
|
|
38
|
-
`grok-*` ids.
|
|
39
|
-
|
|
38
|
+
`grok-*` ids. Codex `web_search` is xAI's server-side tool, not a function
|
|
39
|
+
the bridge runs. See Release history.
|
|
40
|
+
|
|
41
|
+
## Release history
|
|
42
|
+
|
|
43
|
+
What each recent version added. Older cuts are in `CHANGELOG.md`.
|
|
44
|
+
|
|
45
|
+
### 1.7.2 — 2026-09-30
|
|
46
|
+
|
|
47
|
+
- Readable compaction summaries are passed through as user messages. A compaction item that only carries encrypted or internal fields is still dropped.
|
|
48
|
+
- `grok_bridge_generate_video`. The bridge declares this tool and, when the model calls it, posts to `https://api.x.ai/v1/videos/generations` with the grok login bearer, polls `GET /videos/{request_id}`, and returns the video URL as the tool result. A refusal from that API stays a tool error and does not fail the turn.
|
|
49
|
+
- Readable text from Codex input items that used to be dropped is forwarded as user `input_text`: shell, search, tool-search, a revised image prompt, and custom tool text. Encrypted blobs and image-byte results stay dropped.
|
|
50
|
+
- `POST /v1/images/generations` and `POST /v1/images/edits` are forwarded to the Imagine API with the grok login bearer. Codex's `gpt-image-2` body is rewritten to `grok-imagine-image-quality` and `b64_json`. OpenAI `file_id` edits are refused. No `XAI_API_KEY`.
|
|
51
|
+
- Codex `web_search` is kept as one server-side `{type:"web_search"}` tool. `web_search_call` results are passed through to Codex.
|
|
52
|
+
- Voice WebRTC (`POST /v1/realtime/calls`) and Codex cloud tasks are not in this package.
|
|
53
|
+
|
|
54
|
+
### 1.7.1 — 2026-09-30
|
|
55
|
+
|
|
56
|
+
- The bridge holds the upstream reply and sends it to Codex only after `response.completed`. A reset before that (`ECONNRESET`, `EPIPE`, `UND_ERR_SOCKET`, or a close before the reply finishes) is retried up to two more times, and only if Codex has not been sent a byte. This provider's Codex `request_max_retries` and `stream_max_retries` are 0. A retry sends the prompt again, so that attempt's input tokens can be billed again.
|
|
57
|
+
- When `grok --version` fails or does not report a version, the client version is `unknown` instead of the stale `1.0.24`.
|
|
58
|
+
|
|
59
|
+
### 1.7.0 — 2026-09-29
|
|
60
|
+
|
|
61
|
+
- One `x-grok-conv-id` per Codex thread, and the forwarded transcript prefix stays byte-stable so the prompt cache can hit. The full transcript is still sent.
|
|
62
|
+
- Upstream `cached_prompt_tokens` and `cache_read_input_tokens` are copied onto `response.completed` usage and the diagnostics log when the proxy sends them, including `0`. Missing counters are not invented.
|
|
63
|
+
|
|
64
|
+
### 1.6.1 — 2026-09-28
|
|
65
|
+
|
|
66
|
+
- Darwin bundled CLI resolves `Codex.app/Contents/Resources/codex-cli/bin/codex` when that file exists (Codex 26.924). The legacy `Contents/Resources/codex` path remains the fallback. `CODEX_BINARY` still wins. Linux and Windows layouts are unchanged.
|
|
67
|
+
|
|
68
|
+
### 1.6.0 — 2026-09-27
|
|
69
|
+
|
|
70
|
+
- Default catalog model is `grok-4.7` (`Grok 4.7 / xAI`). `grok-4.6` stays listed so existing threads still resolve.
|
|
71
|
+
- `grok-*` still routes to `grok_build_cli`. No other model ids were added. `GROK_BRIDGE_MODELS` still appends extra `grok-*` ids.
|
|
40
72
|
|
|
41
73
|
## Requirements
|
|
42
74
|
|
|
@@ -72,7 +104,7 @@ starts, and tears the provider down with that process.
|
|
|
72
104
|
```sh
|
|
73
105
|
git clone https://github.com/deximple/codex-grok-bridge.git
|
|
74
106
|
cd codex-grok-bridge
|
|
75
|
-
npm test #
|
|
107
|
+
npm test # 191 tests, no network, no inference
|
|
76
108
|
node scripts/codex-grok.mjs
|
|
77
109
|
```
|
|
78
110
|
|
|
@@ -107,8 +139,8 @@ launches from its usual icon.
|
|
|
107
139
|
2. The wrapper adds Grok to the model catalog and sets the provider of a new
|
|
108
140
|
Grok thread to `grok_build_cli`.
|
|
109
141
|
3. Codex `/v1/responses` requests go to the localhost bridge.
|
|
110
|
-
4. The bridge flattens Codex tools
|
|
111
|
-
|
|
142
|
+
4. The bridge flattens Codex function and custom tools into function tools,
|
|
143
|
+
forwards `web_search` as xAI's server-side tool, then pipes the
|
|
112
144
|
`cli-chat-proxy.grok.com` Responses stream through.
|
|
113
145
|
5. Tool results return as the next Codex request `input`.
|
|
114
146
|
|
|
@@ -120,10 +152,27 @@ address when one exists, so a 5–20 s tool gap does not force a fresh name
|
|
|
120
152
|
lookup every time. `GROK_BRIDGE_TRANSPORT=fetch` restores the older `fetch`
|
|
121
153
|
path.
|
|
122
154
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
155
|
+
The bridge holds Grok’s reply until it finishes (`response.completed`). No
|
|
156
|
+
byte of that reply is sent to Codex before then. If the connection drops
|
|
157
|
+
first — a reset (`ECONNRESET`, `EPIPE`, `UND_ERR_SOCKET`) or a close before
|
|
158
|
+
the reply finishes — the bridge sends the same request again, up to two more
|
|
159
|
+
times. DNS and connection failures use that same budget. This provider sets
|
|
160
|
+
Codex `request_max_retries` and `stream_max_retries` to 0, so Codex does not
|
|
161
|
+
send the prompt again as well. If any byte of the reply was already sent to
|
|
162
|
+
Codex, the bridge does not resubmit. A retry sends the prompt to Grok again,
|
|
163
|
+
so that attempt’s input tokens can be billed again. This replaces Codex
|
|
164
|
+
resending the prompt up to two extra times. User aborts and deterministic 422
|
|
165
|
+
responses are not retried. Codex still gives the stream five minutes of
|
|
166
|
+
silence (`stream_idle_timeout_ms`); a reply that takes longer can be cut off
|
|
167
|
+
before any of it is sent.
|
|
168
|
+
|
|
169
|
+
### In short
|
|
170
|
+
|
|
171
|
+
Codex keeps the conversation and sends the whole turn each time. Grok can
|
|
172
|
+
treat the unchanged beginning as a cache. If the connection drops before
|
|
173
|
+
Codex has been sent any bytes, the bridge tries again. Codex does not send
|
|
174
|
+
that same request again. Voice WebRTC (`POST /v1/realtime/calls`) and
|
|
175
|
+
Codex cloud tasks are not in this package.
|
|
127
176
|
|
|
128
177
|
If the Responses path misbehaves, `GROK_BRIDGE_INFERENCE=cli` falls back to the
|
|
129
178
|
older CLI envelope. That path pastes the whole JSON into a prompt each turn, so
|
|
@@ -239,15 +288,15 @@ ephemeral threads and child agents refuse a switch. Other subscribers can block
|
|
|
239
288
|
the reload; if they do, no inference is sent. Pick the model you want when
|
|
240
289
|
starting a new thread, before the first turn is saved.
|
|
241
290
|
|
|
242
|
-
### Not
|
|
291
|
+
### Not in this release
|
|
243
292
|
|
|
244
|
-
Voice
|
|
293
|
+
Voice WebRTC (`POST /v1/realtime/calls`) and Codex cloud tasks are not in this package.
|
|
245
294
|
|
|
246
295
|
Upstream sometimes resets the connection mid-response (three measured cases:
|
|
247
|
-
25 s / 27 s / 253 s, 726 KB–22 MB). The bridge
|
|
248
|
-
|
|
249
|
-
to
|
|
250
|
-
|
|
296
|
+
25 s / 27 s / 253 s, 726 KB–22 MB). The bridge holds the reply and, if that
|
|
297
|
+
drop happens before Codex has been sent any of it, tries the same request up
|
|
298
|
+
to two more times. Codex does not also resend it. A retry can bill the input
|
|
299
|
+
tokens for that attempt again.
|
|
251
300
|
|
|
252
301
|
## Diagnostics
|
|
253
302
|
|
|
@@ -291,7 +340,7 @@ Start here when something breaks. Do not open `~/.grok/auth.json` or
|
|
|
291
340
|
## Verify
|
|
292
341
|
|
|
293
342
|
```sh
|
|
294
|
-
npm test #
|
|
343
|
+
npm test # 191 tests, no remote inference
|
|
295
344
|
npm run test:coverage # 80% line / branch / function gate
|
|
296
345
|
npm run verify:app-server # real app-server routing; also runs against an installed bundle
|
|
297
346
|
npm audit --omit=dev
|
|
@@ -360,14 +409,46 @@ Grok 4.7을 Codex 모델 목록에 넣고, Codex의 `/v1/responses`를
|
|
|
360
409
|
calling으로 옮기고, 상류 Responses 스트림을 전달한 뒤, Codex가 알아보는
|
|
361
410
|
이름으로 되돌립니다.
|
|
362
411
|
|
|
363
|
-
`src/` 아래 파일
|
|
412
|
+
`src/` 아래 파일 18개. 런타임 의존성 없음. Node.js 22 이상. 1.5.0부터 npm
|
|
364
413
|
`"os"`는 `darwin` / `linux` / `win32`입니다.
|
|
365
414
|
|
|
366
415
|
코드 리뷰 전용 제품이 아니고 Grok 네이티브 검색 제품도 아닙니다. 피커에는
|
|
367
416
|
`grok-4.7`(Grok 4.7 / xAI)이 기본으로 보이고 `grok-4.6`도 남습니다. 그 외
|
|
368
417
|
`grok-*` id도 같은 경로로 붙고, `GROK_BRIDGE_MODELS`로 카탈로그에 더합니다.
|
|
369
|
-
Codex
|
|
370
|
-
|
|
418
|
+
Codex `web_search`는 브리지가 실행하는 함수가 아니라 xAI 서버 도구입니다.
|
|
419
|
+
릴리스 기록을 보세요.
|
|
420
|
+
|
|
421
|
+
## 릴리스 기록
|
|
422
|
+
|
|
423
|
+
최근 버전이 더한 것입니다. 그 이전은 `CHANGELOG.md`에 있습니다.
|
|
424
|
+
|
|
425
|
+
### 1.7.2 — 2026-09-30
|
|
426
|
+
|
|
427
|
+
- 읽을 수 있는 compaction 요약을 사용자 메시지로 넘깁니다. 암호화된 내용이나 내부 필드만 있는 compaction 항목은 그대로 버립니다.
|
|
428
|
+
- `grok_bridge_generate_video`. 브리지가 이 도구를 선언하고, 모델이 호출하면 grok 로그인 bearer로 `https://api.x.ai/v1/videos/generations`에 보낸 뒤 `GET /videos/{request_id}`를 폴링하고, 영상 URL을 도구 결과로 돌려줍니다. 그 API의 거절은 도구 오류로 남고 턴을 실패시키지 않습니다.
|
|
429
|
+
- 예전에는 버리던 Codex 입력 항목의 읽을 수 있는 글을 사용자 `input_text`로 넘깁니다. 셸, 검색, tool-search, 수정된 이미지 프롬프트, 커스텀 도구 텍스트입니다. 암호화된 blob과 이미지 바이트 결과는 그대로 버립니다.
|
|
430
|
+
- `POST /v1/images/generations`와 `POST /v1/images/edits`를 grok 로그인 bearer로 Imagine API에 넘깁니다. Codex의 `gpt-image-2` 본문은 `grok-imagine-image-quality`와 `b64_json`으로 바꿉니다. OpenAI `file_id` 편집은 거절합니다. `XAI_API_KEY`는 쓰지 않습니다.
|
|
431
|
+
- Codex `web_search`는 서버 측 `{type:"web_search"}` 도구 하나로 유지합니다. `web_search_call` 결과는 Codex로 통과합니다.
|
|
432
|
+
- 음성 WebRTC(`POST /v1/realtime/calls`)와 Codex 클라우드 작업은 이 패키지에 없습니다.
|
|
433
|
+
|
|
434
|
+
### 1.7.1 — 2026-09-30
|
|
435
|
+
|
|
436
|
+
- 브리지는 상류 응답을 `response.completed` 이후에만 Codex로 보냅니다. 그 전의 리셋(`ECONNRESET`, `EPIPE`, `UND_ERR_SOCKET`, 또는 응답이 끝나기 전의 닫힘)은 최대 두 번 더 재시도하며, Codex에 바이트를 아직 보내지 않았을 때만 합니다. 이 provider의 Codex `request_max_retries`와 `stream_max_retries`는 0입니다. 재시도는 프롬프트를 다시 보내므로 그 시도의 입력 토큰이 다시 과금될 수 있습니다.
|
|
437
|
+
- `grok --version`이 실패하거나 버전을 보고하지 않으면 클라이언트 버전은 오래된 `1.0.24` 대신 `unknown`입니다.
|
|
438
|
+
|
|
439
|
+
### 1.7.0 — 2026-09-29
|
|
440
|
+
|
|
441
|
+
- Codex 스레드마다 `x-grok-conv-id`는 하나이고, 전달하는 트랜스크립트 접두는 바이트 그대로라 프롬프트 캐시가 맞을 수 있습니다. 전체 트랜스크립트는 그대로 보냅니다.
|
|
442
|
+
- 상류가 보내면 `cached_prompt_tokens`와 `cache_read_input_tokens`를 `response.completed` usage와 진단 로그에 복사합니다. `0`도 포함합니다. 없는 카운터는 만들지 않습니다.
|
|
443
|
+
|
|
444
|
+
### 1.6.1 — 2026-09-28
|
|
445
|
+
|
|
446
|
+
- Darwin 번들 CLI는 파일이 있으면 `Codex.app/Contents/Resources/codex-cli/bin/codex`를 씁니다(Codex 26.924). 예전 `Contents/Resources/codex`는 폴백으로 남습니다. `CODEX_BINARY`가 우선합니다. Linux와 Windows 배치는 그대로입니다.
|
|
447
|
+
|
|
448
|
+
### 1.6.0 — 2026-09-27
|
|
449
|
+
|
|
450
|
+
- 기본 카탈로그 모델은 `grok-4.7`(Grok 4.7 / xAI)입니다. `grok-4.6`은 기존 스레드가 계속 맞게 목록에 남습니다.
|
|
451
|
+
- `grok-*`는 여전히 `grok_build_cli`로 갑니다. 다른 모델 id는 추가하지 않았습니다. `GROK_BRIDGE_MODELS`는 그 외 `grok-*` id를 카탈로그에 더합니다.
|
|
371
452
|
|
|
372
453
|
## 필요한 것
|
|
373
454
|
|
|
@@ -402,7 +483,7 @@ codex-grok exec --skip-git-repo-check --sandbox workspace-write '작업 내용'
|
|
|
402
483
|
```sh
|
|
403
484
|
git clone https://github.com/deximple/codex-grok-bridge.git
|
|
404
485
|
cd codex-grok-bridge
|
|
405
|
-
npm test #
|
|
486
|
+
npm test # 191건, 네트워크·추론 없음
|
|
406
487
|
node scripts/codex-grok.mjs
|
|
407
488
|
```
|
|
408
489
|
|
|
@@ -436,8 +517,8 @@ Codex 창에도 보일 수 있습니다.
|
|
|
436
517
|
2. 래퍼가 모델 목록에 Grok를 추가하고, 새 Grok 작업의 제공자를
|
|
437
518
|
`grok_build_cli`로 설정합니다.
|
|
438
519
|
3. Codex의 `/v1/responses` 요청은 localhost 브리지로 갑니다.
|
|
439
|
-
4. 브리지는 Codex
|
|
440
|
-
|
|
520
|
+
4. 브리지는 Codex 함수·커스텀 도구를 function tool로 펼치고, `web_search`는
|
|
521
|
+
xAI 서버 도구로 넘긴 뒤 `cli-chat-proxy.grok.com` Responses 스트림을 이어줍니다.
|
|
441
522
|
5. 도구 결과는 다음 Codex 요청의 `input`으로 돌아갑니다.
|
|
442
523
|
|
|
443
524
|
인증은 `grok login` 세션입니다. `XAI_API_KEY`는 쓰지 않습니다.
|
|
@@ -447,10 +528,25 @@ Codex 창에도 보일 수 있습니다.
|
|
|
447
528
|
실행으로 5–20초가 비어도 매번 이름을 다시 찾지 않기 위해서입니다.
|
|
448
529
|
`GROK_BRIDGE_TRANSPORT=fetch`로 이전 `fetch` 경로로 되돌릴 수 있습니다.
|
|
449
530
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
531
|
+
브리지는 Grok의 응답이 끝날 때까지(`response.completed`) 들고 있습니다.
|
|
532
|
+
그 전에 Codex로 응답 바이트를 보내지 않습니다. 그 전에 연결이 끊기면 —
|
|
533
|
+
리셋(`ECONNRESET`, `EPIPE`, `UND_ERR_SOCKET`)이거나, 응답이 끝나기 전에
|
|
534
|
+
닫힌 경우 — 브리지가 같은 요청을 최대 두 번 더 보냅니다. DNS·연결 실패도
|
|
535
|
+
같은 횟수를 씁니다. 이 provider는 Codex `request_max_retries`와
|
|
536
|
+
`stream_max_retries`를 0으로 두어, Codex가 그 프롬프트를 또 보내지 않게
|
|
537
|
+
합니다. 응답 바이트를 Codex에 이미 보냈다면 브리지는 다시 보내지 않습니다.
|
|
538
|
+
재시도는 프롬프트를 Grok에 다시 보내므로, 그 시도의 입력 토큰이 다시 과금될
|
|
539
|
+
수 있습니다. Codex가 프롬프트를 최대 두 번 더 보내던 것을 이것으로 대신합니다.
|
|
540
|
+
사용자 중단과 422 같은 결정적 거절은 재시도하지 않습니다. Codex는 응답
|
|
541
|
+
바이트 없이 5분(`stream_idle_timeout_ms`)을 기다립니다. 그보다 오래 걸리면
|
|
542
|
+
보내기 전에 끊길 수 있습니다.
|
|
543
|
+
|
|
544
|
+
### 쉽게 말하면
|
|
545
|
+
|
|
546
|
+
Codex가 대화를 갖고 있고, 턴마다 그 턴 전체를 보냅니다. 앞부분이 그대로면
|
|
547
|
+
Grok는 그 부분을 캐시로 볼 수 있습니다. Codex에 바이트를 보내기 전에 연결이
|
|
548
|
+
끊기면 브리지가 다시 시도합니다. Codex는 그 요청을 또 보내지 않습니다. 음성
|
|
549
|
+
WebRTC(`POST /v1/realtime/calls`)와 Codex 클라우드 작업은 이 패키지에 없습니다.
|
|
454
550
|
|
|
455
551
|
Responses 경로가 이상하면 `GROK_BRIDGE_INFERENCE=cli`로 이전 CLI 봉투 경로를
|
|
456
552
|
씁니다. 매 턴 전체 JSON을 프롬프트로 넣으므로 더 느리고 비싸며, 토큰 단위
|
|
@@ -558,15 +654,14 @@ OpenAI 경로가 맞습니다.
|
|
|
558
654
|
구독자가 재로드를 막으면 추론을 보내지 않습니다. 첫 턴이 저장되기 전에 새
|
|
559
655
|
작업을 시작할 때 원하는 모델을 고르세요.
|
|
560
656
|
|
|
561
|
-
###
|
|
657
|
+
### 이번 릴리스에 없는 것
|
|
562
658
|
|
|
563
|
-
|
|
659
|
+
음성 WebRTC(`POST /v1/realtime/calls`)와 Codex 클라우드 작업은 이 패키지에 없습니다.
|
|
564
660
|
|
|
565
661
|
상류가 응답 중간에 연결을 리셋하는 경우가 있습니다(실측 3건: 25초 / 27초 /
|
|
566
|
-
253초, 726 KB–22 MB).
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
재시도할 수 있습니다.
|
|
662
|
+
253초, 726 KB–22 MB). 브리지는 응답을 들고 있다가, Codex에 그 응답을 보내기
|
|
663
|
+
전에 끊기면 같은 요청을 최대 두 번 더 시도합니다. Codex가 또 보내지는
|
|
664
|
+
않습니다. 재시도하면 그 시도의 입력 토큰이 다시 과금될 수 있습니다.
|
|
570
665
|
|
|
571
666
|
## 진단 로그
|
|
572
667
|
|
|
@@ -609,7 +704,7 @@ provider에 Codex `request_max_retries` / `stream_max_retries`를 2로 두어
|
|
|
609
704
|
## 검증
|
|
610
705
|
|
|
611
706
|
```sh
|
|
612
|
-
npm test #
|
|
707
|
+
npm test # 191건, 외부 추론 없음
|
|
613
708
|
npm run test:coverage # line/branch/function 80% 게이트
|
|
614
709
|
npm run verify:app-server # 실제 app-server 라우팅. 설치된 앱 번들에서도 실행
|
|
615
710
|
npm audit --omit=dev
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-grok-bridge",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "Run Grok 4.7 as the model inside Codex, with Codex still owning tools, permissions, history and MCP. Uses the grok login session, not an API key.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/bridge.mjs
CHANGED
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
parseGrokResult,
|
|
9
9
|
runGrok,
|
|
10
10
|
} from "./cli-inference.mjs";
|
|
11
|
-
import {
|
|
11
|
+
import { emitProxySse, readRelayProxySse } from "./proxy.mjs";
|
|
12
|
+
import { videoCallsFromParts, videoToolOutput } from "./videogen.mjs";
|
|
13
|
+
import { forwardImagine } from "./imagine.mjs";
|
|
12
14
|
import {
|
|
13
15
|
applyCacheUsage,
|
|
14
16
|
createPrefixMemory,
|
|
@@ -23,6 +25,89 @@ import { catalogModelInfos, isGrokModel, MODEL_INFO } from "./models.mjs";
|
|
|
23
25
|
|
|
24
26
|
export { MODEL_INFO };
|
|
25
27
|
|
|
28
|
+
const VIDEO_FOLLOW_UPS = 2;
|
|
29
|
+
|
|
30
|
+
async function relayWithVideo(options, output, map, usageBox, onClientByte) {
|
|
31
|
+
let body = options.body;
|
|
32
|
+
for (let step = 0; step <= VIDEO_FOLLOW_UPS; step += 1) {
|
|
33
|
+
const parts = await readRelayProxySse({ ...options, body }, output);
|
|
34
|
+
const calls =
|
|
35
|
+
process.env.GROK_BRIDGE_VIDEO_GEN === "off" ? [] : videoCallsFromParts(parts);
|
|
36
|
+
if (!calls.length || step === VIDEO_FOLLOW_UPS) {
|
|
37
|
+
await emitProxySse(parts, output, map, usageBox, onClientByte);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// The videos API poll can sit for minutes. Keepalive is safe here: this
|
|
41
|
+
// upstream reply is finished, and nothing has been written to Codex yet.
|
|
42
|
+
onClientByte();
|
|
43
|
+
const additions = [];
|
|
44
|
+
for (const call of calls) {
|
|
45
|
+
const toolOutput = await videoToolOutput(call, options.video);
|
|
46
|
+
additions.push(
|
|
47
|
+
{
|
|
48
|
+
type: "function_call",
|
|
49
|
+
name: call.name,
|
|
50
|
+
call_id: call.call_id,
|
|
51
|
+
arguments: call.arguments,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: "function_call_output",
|
|
55
|
+
name: call.name,
|
|
56
|
+
call_id: call.call_id,
|
|
57
|
+
output: toolOutput,
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
body = { ...body, input: [...body.input, ...additions] };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function relayImagine(req, res, json, kind, options) {
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
res.on("close", () => controller.abort());
|
|
68
|
+
let body;
|
|
69
|
+
try {
|
|
70
|
+
const chunks = [];
|
|
71
|
+
let size = 0;
|
|
72
|
+
const limit = options.maxBodyBytes ?? 40 * 1024 * 1024;
|
|
73
|
+
for await (const chunk of req) {
|
|
74
|
+
size += chunk.length;
|
|
75
|
+
if (size > limit) return json(413, { error: "Request too large" });
|
|
76
|
+
chunks.push(chunk);
|
|
77
|
+
}
|
|
78
|
+
body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
79
|
+
} catch {
|
|
80
|
+
return json(400, { error: "Invalid request" });
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const session = options.grokSession ?? readGrokBearerToken(options.grokHome);
|
|
84
|
+
const response = await forwardImagine({
|
|
85
|
+
kind,
|
|
86
|
+
body,
|
|
87
|
+
token: session.token,
|
|
88
|
+
fetchImpl: options.imagineFetch,
|
|
89
|
+
baseUrl: options.imagineBaseUrl,
|
|
90
|
+
signal: controller.signal,
|
|
91
|
+
});
|
|
92
|
+
if (!res.writableEnded && !res.destroyed) json(200, response);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (res.writableEnded || res.destroyed) return;
|
|
95
|
+
if (error instanceof GrokAuthError) return json(401, { error: error.message });
|
|
96
|
+
const status = Number(error?.status);
|
|
97
|
+
const message = String(error?.message ?? "");
|
|
98
|
+
if (
|
|
99
|
+
status >= 400 &&
|
|
100
|
+
status < 600 &&
|
|
101
|
+
(message.startsWith("Image generation") ||
|
|
102
|
+
message.startsWith("Image edit") ||
|
|
103
|
+
message.startsWith("OpenAI file") ||
|
|
104
|
+
message.startsWith("Grok login"))
|
|
105
|
+
)
|
|
106
|
+
return json(status, { error: message });
|
|
107
|
+
return json(502, { error: "Image generation failed." });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
26
111
|
export function publicBridgeError(error, context = {}) {
|
|
27
112
|
if (error instanceof GrokAuthError) return error.message;
|
|
28
113
|
const message = String(error?.message ?? "");
|
|
@@ -51,13 +136,20 @@ export function createBridgeServer(options = {}) {
|
|
|
51
136
|
const route = new URL(req.url, "http://127.0.0.1").pathname;
|
|
52
137
|
if (req.method === "GET" && route === "/v1/models")
|
|
53
138
|
return json(200, { models: catalogModelInfos() });
|
|
54
|
-
|
|
139
|
+
const imagineKind =
|
|
140
|
+
route === "/v1/images/generations"
|
|
141
|
+
? "generations"
|
|
142
|
+
: route === "/v1/images/edits"
|
|
143
|
+
? "edits"
|
|
144
|
+
: null;
|
|
145
|
+
if (req.method !== "POST" || (!imagineKind && route !== "/v1/responses"))
|
|
55
146
|
return json(404, { error: "Not found" });
|
|
56
147
|
const auth = Buffer.from(req.headers.authorization ?? "");
|
|
57
148
|
if (auth.length !== token.length || !timingSafeEqual(auth, token))
|
|
58
149
|
return json(401, { error: "Unauthorized" });
|
|
59
150
|
if (req.headers.origin)
|
|
60
151
|
return json(403, { error: "Browser requests are not accepted" });
|
|
152
|
+
if (imagineKind) return relayImagine(req, res, json, imagineKind, options);
|
|
61
153
|
let body;
|
|
62
154
|
let requestBytes = 0;
|
|
63
155
|
try {
|
|
@@ -101,8 +193,14 @@ export function createBridgeServer(options = {}) {
|
|
|
101
193
|
);
|
|
102
194
|
const id = "resp_" + randomUUID();
|
|
103
195
|
const startedAt = Date.now();
|
|
196
|
+
// A keepalive comment is an SSE byte. Writing one before the reply finishes
|
|
197
|
+
// would make a later upstream reset unsafe to retry, so the proxy path
|
|
198
|
+
// stays quiet until the first real event is handed to Codex. The CLI path
|
|
199
|
+
// writes its own events and may keep the socket warm while it runs.
|
|
200
|
+
let responseStarted = false;
|
|
104
201
|
const keepalive = setInterval(() => {
|
|
105
|
-
if (!res.destroyed
|
|
202
|
+
if (!responseStarted || res.destroyed || res.writableEnded) return;
|
|
203
|
+
res.write(": keepalive\n\n");
|
|
106
204
|
}, 10000);
|
|
107
205
|
const useCli =
|
|
108
206
|
Boolean(options.runGrok) ||
|
|
@@ -125,6 +223,7 @@ export function createBridgeServer(options = {}) {
|
|
|
125
223
|
acquired = true;
|
|
126
224
|
queuedMs = Date.now() - queuedAt;
|
|
127
225
|
if (useCli) {
|
|
226
|
+
responseStarted = true;
|
|
128
227
|
event("response.created", { response: { id } });
|
|
129
228
|
const invocation = buildGrokInvocation(body, options);
|
|
130
229
|
invocation.threadId = req.headers["thread-id"];
|
|
@@ -158,7 +257,7 @@ export function createBridgeServer(options = {}) {
|
|
|
158
257
|
},
|
|
159
258
|
});
|
|
160
259
|
} else {
|
|
161
|
-
const session = readGrokBearerToken(options.grokHome);
|
|
260
|
+
const session = options.grokSession ?? readGrokBearerToken(options.grokHome);
|
|
162
261
|
// Grok takes input_image blocks natively; only unusable attachments
|
|
163
262
|
// are swapped for an explanation, so one bad image cannot make the
|
|
164
263
|
// upstream reject the whole conversation.
|
|
@@ -171,25 +270,39 @@ export function createBridgeServer(options = {}) {
|
|
|
171
270
|
if (convId) request.prompt_cache_key = convId;
|
|
172
271
|
request.input = prefixes.reuse(convId, projected);
|
|
173
272
|
const usageBox = {};
|
|
174
|
-
const proxy = await openProxyStreamWithRetry({
|
|
175
|
-
token: session.token,
|
|
176
|
-
userId: session.userId,
|
|
177
|
-
body: request,
|
|
178
|
-
signal: controller.signal,
|
|
179
|
-
fetchImpl: options.proxyFetch,
|
|
180
|
-
baseUrl: options.proxyBaseUrl,
|
|
181
|
-
convId,
|
|
182
|
-
sessionId: Array.isArray(threadId) ? threadId[0] : threadId,
|
|
183
|
-
onRetry: ({ attempt, kind }) =>
|
|
184
|
-
diagnostics.record({
|
|
185
|
-
event: "turn_retried",
|
|
186
|
-
kind,
|
|
187
|
-
attempt,
|
|
188
|
-
elapsedMs: Date.now() - startedAt,
|
|
189
|
-
}),
|
|
190
|
-
});
|
|
191
273
|
try {
|
|
192
|
-
await
|
|
274
|
+
await relayWithVideo(
|
|
275
|
+
{
|
|
276
|
+
token: session.token,
|
|
277
|
+
userId: session.userId,
|
|
278
|
+
body: request,
|
|
279
|
+
signal: controller.signal,
|
|
280
|
+
fetchImpl: options.proxyFetch,
|
|
281
|
+
baseUrl: options.proxyBaseUrl,
|
|
282
|
+
convId,
|
|
283
|
+
sessionId: Array.isArray(threadId) ? threadId[0] : threadId,
|
|
284
|
+
onRetry: ({ attempt, kind }) =>
|
|
285
|
+
diagnostics.record({
|
|
286
|
+
event: "turn_retried",
|
|
287
|
+
kind,
|
|
288
|
+
attempt,
|
|
289
|
+
elapsedMs: Date.now() - startedAt,
|
|
290
|
+
}),
|
|
291
|
+
video: {
|
|
292
|
+
token: session.token,
|
|
293
|
+
fetchImpl: options.videoFetch,
|
|
294
|
+
baseUrl: options.videoBaseUrl,
|
|
295
|
+
pause: options.videoPause,
|
|
296
|
+
signal: controller.signal,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
res,
|
|
300
|
+
map,
|
|
301
|
+
usageBox,
|
|
302
|
+
() => {
|
|
303
|
+
responseStarted = true;
|
|
304
|
+
},
|
|
305
|
+
);
|
|
193
306
|
} finally {
|
|
194
307
|
cacheUsage = usageBox.cacheUsage ?? null;
|
|
195
308
|
}
|
package/src/errors.mjs
CHANGED
|
@@ -40,7 +40,14 @@ const CONNECT_CODES = new Set([
|
|
|
40
40
|
"DEPTH_ZERO_SELF_SIGNED_CERT", "ERR_TLS_CERT_ALTNAME_INVALID",
|
|
41
41
|
]);
|
|
42
42
|
const TIMEOUT_CODES = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT"]);
|
|
43
|
-
const CLOSED_CODES = new Set([
|
|
43
|
+
const CLOSED_CODES = new Set([
|
|
44
|
+
"UND_ERR_SOCKET",
|
|
45
|
+
"ECONNRESET",
|
|
46
|
+
"EPIPE",
|
|
47
|
+
// The body ended without response.completed. Same class as a socket reset:
|
|
48
|
+
// the reply never finished, and Codex has not been given a partial one.
|
|
49
|
+
"ERR_UPSTREAM_PREMATURE_CLOSE",
|
|
50
|
+
]);
|
|
44
51
|
const ABORT_CODES = new Set([
|
|
45
52
|
"ABORT_ERR", "ERR_STREAM_DESTROYED", "ERR_STREAM_WRITE_AFTER_END", "ERR_STREAM_PREMATURE_CLOSE",
|
|
46
53
|
]);
|