From 4d74d8d4278b5016e27c80b0aa3bd0d238910fec Mon Sep 17 00:00:00 2001 From: Bet4 <0xbet4@gmail.com> Date: Thu, 20 Aug 2026 00:41:09 +0800 Subject: [PATCH 1/2] fix(responses): backfill missing id on output items for strict decoders Some upstream relays omit the required id field on Responses output items (message, reasoning, function_call) in response.completed and output_item.added/done events. Strict serde decoders like grok-build's async-openai fork fail with 'missing field id' when deserializing these events, breaking grok CLI over the Responses protocol. Generate a deterministic id per (item type, output index) using the canonical OpenAI id prefixes (msg_, rs_, fc_, ws_, fs_, ci_, cc_, ig_). Existing ids are never overwritten. The same index is used across streaming events so the id stays stable for one item. Both the SSE block rewrite path and the bounded-JSON passthrough path are covered. --- .../responses/responses-field-backfill.ts | 42 +++++- tests/responses-field-backfill.test.ts | 140 ++++++++++++++++++ 2 files changed, 176 insertions(+), 6 deletions(-) diff --git a/src/server/responses/responses-field-backfill.ts b/src/server/responses/responses-field-backfill.ts index dff2da1158..ac06c61c56 100644 --- a/src/server/responses/responses-field-backfill.ts +++ b/src/server/responses/responses-field-backfill.ts @@ -29,6 +29,32 @@ function isPlainObject(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value); } +/** Wire prefixes for Responses output item ids, matching OpenAI's id shapes. */ +const ITEM_ID_PREFIXES: Readonly> = { + message: "msg_", + reasoning: "rs_", + function_call: "fc_", + web_search_call: "ws_", + file_search_call: "fs_", + code_interpreter_call: "ci_", + computer_call: "cc_", + image_gen_call: "ig_", +}; + +/** + * Backfill a required id on an output item when absent. Strict Responses + * decoders (e.g. grok-build serde types) fail with "missing field id" when a + * message or reasoning item has no id, which some upstream relays omit. The + * generated id is deterministic per (type, output index) so it stays stable + * across streaming events that reference the same item. + */ +function backfillItemId(item: Record, outputIndex: number): Record { + if (typeof item.id === "string" && item.id.length > 0) return item; + const type = typeof item.type === "string" ? item.type : ""; + const prefix = Object.prototype.hasOwnProperty.call(ITEM_ID_PREFIXES, type) ? ITEM_ID_PREFIXES[type] : "item_"; + return { ...item, id: prefix + "ocx_" + outputIndex }; +} + /** * Backfill annotations: [] on an output_text content part if missing. * Returns the same object reference if no change is needed. @@ -60,14 +86,16 @@ function backfillContentArray(content: unknown): unknown { /** * Walk an output item and backfill output_text parts in its content. + * Also backfills a missing required id on the item itself. * Returns the same object reference if nothing changed. */ -function backfillOutputItem(item: unknown): unknown { +function backfillOutputItem(item: unknown, outputIndex: number): unknown { if (!isPlainObject(item)) return item; const content = item.content; const repaired = backfillContentArray(content); - if (repaired === content) return item; - return { ...item, content: repaired }; + const withId = backfillItemId(item, outputIndex); + if (repaired === content && withId === item) return item; + return { ...withId, ...(repaired === content ? {} : { content: repaired }) }; } /** @@ -79,9 +107,9 @@ function backfillResponseOutput(response: unknown): unknown { const output = response.output; if (!Array.isArray(output)) return response; let changed = false; - const repaired = output.map((item) => { + const repaired = output.map((item, idx) => { if (!isPlainObject(item)) return item; - const next = backfillOutputItem(item); + const next = backfillOutputItem(item, idx); if (next !== item) changed = true; return next; }); @@ -100,7 +128,9 @@ function rewriteEvent(event: Record): Record { // output_item.added / output_item.done: item.content[] -> output_text parts if ((type === "response.output_item.added" || type === "response.output_item.done") && isPlainObject(event.item)) { - const item = backfillOutputItem(event.item); + const rawIndex = event.output_index; + const index = typeof rawIndex === "number" && Number.isInteger(rawIndex) && rawIndex >= 0 ? rawIndex : 0; + const item = backfillOutputItem(event.item, index); if (item !== event.item) { next = { ...next, item }; changed = true; diff --git a/tests/responses-field-backfill.test.ts b/tests/responses-field-backfill.test.ts index 8071323e5a..9ef4e6fa0f 100644 --- a/tests/responses-field-backfill.test.ts +++ b/tests/responses-field-backfill.test.ts @@ -170,4 +170,144 @@ describe("responses-field-backfill", () => { expect(result.output[0].content[3].annotations).toBe("not-an-array"); expect(result.output[0].content[4].annotations).toEqual({ unexpected: true }); }); + + test("backfills missing ids on response.completed output items", () => { + const event = { + type: "response.completed", + sequence_number: 42, + response: { + id: "resp_1", + object: "response", + status: "completed", + output: [ + { type: "reasoning", summary: [{ type: "summary_text", text: "thinking" }] }, + { + type: "message", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "hello" }], + }, + { + type: "function_call", + call_id: "call_1", + name: "todo_write", + arguments: "{}", + }, + ], + }, + }; + const [out] = apply(sseBlock(event)); + const parsed = parseData([out])[0]; + expect(parsed.response.output[0].id).toBe("rs_ocx_0"); + expect(parsed.response.output[1].id).toBe("msg_ocx_1"); + expect(parsed.response.output[2].id).toBe("fc_ocx_2"); + }); + + test("preserves existing item ids", () => { + const event = { + type: "response.completed", + sequence_number: 42, + response: { + id: "resp_1", + object: "response", + status: "completed", + output: [ + { type: "message", id: "msg_real", role: "assistant", content: [{ type: "output_text", text: "hi" }] }, + ], + }, + }; + const [out] = apply(sseBlock(event)); + const parsed = parseData([out])[0]; + expect(parsed.response.output[0].id).toBe("msg_real"); + }); + + test("uses output_index when backfilling output_item.done id", () => { + const event = { + type: "response.output_item.done", + output_index: 3, + item: { + type: "message", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "hi" }], + }, + }; + const [out] = apply(sseBlock(event)); + const parsed = parseData([out])[0]; + expect(parsed.item.id).toBe("msg_ocx_3"); + }); + + test("falls back to item_ prefix for inherited type names", () => { + const event = { + type: "response.output_item.done", + output_index: 0, + item: { + type: "toString", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "hi" }], + }, + }; + const [out] = apply(sseBlock(event)); + const parsed = parseData([out])[0]; + expect(parsed.item.id).toBe("item_ocx_0"); + }); + + test("rejects invalid output_index and falls back to 0", () => { + for (const badIndex of [-1, 1.5, NaN, Infinity, "0", null, undefined]) { + const event = { + type: "response.output_item.done", + output_index: badIndex, + item: { + type: "message", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "hi" }], + }, + }; + const [out] = apply(sseBlock(event)); + const parsed = parseData([out])[0]; + expect(parsed.item.id).toBe("msg_ocx_0"); + } + }); + + test("backfillResponsesFieldsJson backfills missing ids on output items", () => { + const response = { + id: "resp_1", + object: "response", + status: "completed", + output: [ + { type: "reasoning", summary: [{ type: "summary_text", text: "thinking" }] }, + { + type: "message", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "hello" }], + }, + { + type: "function_call", + call_id: "call_1", + name: "todo_write", + arguments: "{}", + }, + ], + }; + const result = JSON.parse(backfillResponsesFieldsJson(JSON.stringify(response))) as typeof response; + expect(result.output[0].id).toBe("rs_ocx_0"); + expect(result.output[1].id).toBe("msg_ocx_1"); + expect(result.output[2].id).toBe("fc_ocx_2"); + }); + + test("backfillResponsesFieldsJson preserves existing item ids", () => { + const response = { + id: "resp_1", + object: "response", + status: "completed", + output: [ + { type: "message", id: "msg_real", role: "assistant", content: [{ type: "output_text", text: "hi" }] }, + ], + }; + const result = JSON.parse(backfillResponsesFieldsJson(JSON.stringify(response))) as typeof response; + expect(result.output[0].id).toBe("msg_real"); + }); }); From a8b4753b38746b949b8256f5473b1ce831f7568e Mon Sep 17 00:00:00 2001 From: Bet4 <0xbet4@gmail.com> Date: Thu, 20 Aug 2026 00:41:15 +0800 Subject: [PATCH 2/2] docs(grok-build): describe Responses passthrough path consistently The Grok Build guide still referenced Chat Completions transport and its inbound translator, but grok models are now registered with api_backend = "responses" and talk to opencodex over the Responses API. Update all locales: - Replace the endpoint description with POST /v1/responses - Fix the managed-block example api_backend to "responses" - Rewrite the reasoning section to describe Responses passthrough of reasoning.summary instead of Chat Completions reasoning_content translation --- .../src/content/docs/fr/guides/grok-build.md | 15 +++++++------ .../src/content/docs/guides/grok-build.md | 21 +++++++++---------- .../src/content/docs/ja/guides/grok-build.md | 2 +- .../src/content/docs/ko/guides/grok-build.md | 2 +- .../src/content/docs/ru/guides/grok-build.md | 2 +- .../src/content/docs/tr/guides/grok-build.md | 4 +--- .../content/docs/zh-cn/guides/grok-build.md | 2 +- .../content/docs/zh-tw/guides/grok-build.md | 2 +- 8 files changed, 23 insertions(+), 27 deletions(-) diff --git a/docs-site/src/content/docs/fr/guides/grok-build.md b/docs-site/src/content/docs/fr/guides/grok-build.md index 709542ad31..6e3dada1a8 100644 --- a/docs-site/src/content/docs/fr/guides/grok-build.md +++ b/docs-site/src/content/docs/fr/guides/grok-build.md @@ -3,8 +3,8 @@ title: Grok Build description: Utilisez n’importe quel modèle routé par opencodex depuis la CLI Grok Build de xAI — les modèles sont automatiquement enregistrés dans ~/.grok/config.toml pendant l’exécution du proxy. --- -opencodex expose un point de terminaison compatible OpenAI `POST /v1/chat/completions` (ainsi que `/v1/responses`) sur son -port local, tandis que Grok Build prend en charge les modèles personnalisés hébergés sur des serveurs compatibles OpenAI. Avec +opencodex expose un point de terminaison compatible OpenAI `POST /v1/responses` sur son port local, +tandis que Grok Build prend en charge les modèles personnalisés hébergés sur des serveurs compatibles OpenAI. Avec cette intégration, opencodex enregistre automatiquement l’intégralité de son catalogue visible dans Grok Build : aucune modification manuelle de la configuration n’est nécessaire. @@ -61,12 +61,11 @@ dans Codex. Ceux dont la liste de niveaux est vide n’affichent aucun contrôle de Codex. Les entrées GPT-5.6 natives sont distinctes : elles conservent et exposent leurs échelles de raisonnement en amont fixes, et non les métadonnées configurées pour les modèles routés. -Grok Build communique avec opencodex au moyen de Chat Completions et envoie `reasoning_effort` lorsque -l’échelle est annoncée. Dans ce cas, le traducteur Chat Completions entrant définit par défaut le champ Responses -`reasoning.summary` sur `auto` ; les traces de raisonnement parviennent donc à Grok sous la forme -`delta.reasoning_content` au lieu d’être masquées. Réglez `include_reasoning: false` (ou -`reasoning.summary: "none"`) si un client souhaite que le modèle réfléchisse sans renvoyer le -tracé. Une valeur explicite de `reasoning.summary` prévaut lorsque les deux options sont présentes. +Grok Build communique avec opencodex au moyen de l’API Responses. Lorsque la route annonce une échelle +de raisonnement, la passerelle Responses transmet `reasoning.summary` telle que configurée, de sorte que +les traces de raisonnement parviennent nativement à Grok sous forme d’éléments de raisonnement Responses. +Réglez `reasoning.summary: "none"` si un client souhaite que le modèle réfléchisse sans renvoyer le +tracé. Une valeur explicite de `reasoning.summary` prévaut sur la valeur par défaut de la route. ## Note d'authentification diff --git a/docs-site/src/content/docs/guides/grok-build.md b/docs-site/src/content/docs/guides/grok-build.md index 08a1073805..f1192b73e2 100644 --- a/docs-site/src/content/docs/guides/grok-build.md +++ b/docs-site/src/content/docs/guides/grok-build.md @@ -3,10 +3,10 @@ title: Grok Build description: Use any opencodex-routed model from xAI's Grok Build CLI — models are auto-registered into ~/.grok/config.toml while the proxy runs. --- -opencodex serves an OpenAI-compatible `POST /v1/chat/completions` (and `/v1/responses`) on its -local port, and Grok Build supports custom models against OpenAI-compatible servers. Starting -with this integration, opencodex registers its whole visible catalog into Grok Build -automatically — no manual config editing required. +opencodex serves an OpenAI-compatible `POST /v1/responses` on its local port, and Grok Build +supports custom models against OpenAI-compatible servers. Starting with this integration, +opencodex registers its whole visible catalog into Grok Build automatically — no manual config +editing required. ## Auto-registration @@ -18,7 +18,7 @@ into `~/.grok/config.toml`: [model.ocx-gpt-5-6-sol] model = "gpt-5.6-sol" base_url = "http://127.0.0.1:10100/v1" -api_backend = "chat_completions" +api_backend = "responses" api_key = "opencodex-loopback" name = "OCX gpt-5.6-sol" # ... one [model.ocx-*] table per visible model ... @@ -61,12 +61,11 @@ in Codex. Models with an empty tier list keep no effort control, matching Codex behavior. Native GPT-5.6 entries are separate: they preserve and expose their pinned upstream reasoning ladders rather than provider-configured routed metadata. -Grok Build talks to opencodex over Chat Completions and sends `reasoning_effort` when -the ladder is advertised. The Chat Completions inbound translator defaults the internal -Responses `reasoning.summary` to `auto` in that case, so thinking traces reach Grok as -`delta.reasoning_content` instead of being hidden. Set `include_reasoning: false` (or -`reasoning.summary: "none"`) if a client wants the model to think without returning the -trace. An explicit `reasoning.summary` wins when both knobs are present. +Grok Build talks to opencodex over the Responses API. When the route advertises a reasoning +ladder, the Responses passthrough forwards `reasoning.summary` as configured, so thinking +traces reach Grok natively as Responses reasoning items. Set `reasoning.summary: "none"` if +a client wants the model to think without returning the trace. An explicit `reasoning.summary` +wins over the route default. ## Authentication note diff --git a/docs-site/src/content/docs/ja/guides/grok-build.md b/docs-site/src/content/docs/ja/guides/grok-build.md index e68af728ef..564fecf708 100644 --- a/docs-site/src/content/docs/ja/guides/grok-build.md +++ b/docs-site/src/content/docs/ja/guides/grok-build.md @@ -3,7 +3,7 @@ title: グロクビルド description: xAI の Grok Build CLI から opencodex でルーティングされたモデルを使用します。モデルはプロキシの実行中に ~/.grok/config.toml に自動登録されます。 --- -opencodex はローカル ポート上で OpenAI 互換の `POST /v1/chat/completions` (および `/v1/responses`) を提供し、Grok Build は OpenAI 互換サーバーに対するカスタム モデルをサポートします。この統合により、opencodex は表示されているカタログ全体を Grok Build に自動的に登録します。手動による構成編集は必要ありません。 +opencodex はローカル ポート上で OpenAI 互換の `POST /v1/responses` を提供し、Grok Build は OpenAI 互換サーバーに対するカスタム モデルをサポートします。この統合により、opencodex は表示されているカタログ全体を Grok Build に自動的に登録します。手動による構成編集は必要ありません。 ## 自動登録 diff --git a/docs-site/src/content/docs/ko/guides/grok-build.md b/docs-site/src/content/docs/ko/guides/grok-build.md index 1f6b09ecca..a4491aa963 100644 --- a/docs-site/src/content/docs/ko/guides/grok-build.md +++ b/docs-site/src/content/docs/ko/guides/grok-build.md @@ -3,7 +3,7 @@ title: Grok Build 안내 description: xAI의 Grok Build CLI에서 opencodex로 라우팅되는 모든 모델을 사용합니다. 프로세스가 실행되는 동안 모델은 `~/.grok/config.toml`에 자동 등록됩니다. --- -opencodex는 로컬 포트에서 OpenAI 호환 `POST /v1/chat/completions`(및 `/v1/responses`)를 제공합니다. Grok Build는 OpenAI 호환 서버를 상대로 사용자 정의 모델을 지원합니다. 이 통합은 opencodex가 노출하는 전체 카탈로그를 Grok Build에 자동 등록합니다. 수동으로 설정 파일을 편집할 필요가 없습니다. +opencodex는 로컬 포트에서 OpenAI 호환 `POST /v1/responses`를 제공합니다. Grok Build는 OpenAI 호환 서버를 상대로 사용자 정의 모델을 지원합니다. 이 통합은 opencodex가 노출하는 전체 카탈로그를 Grok Build에 자동 등록합니다. 수동으로 설정 파일을 편집할 필요가 없습니다. ## 자동 등록 diff --git a/docs-site/src/content/docs/ru/guides/grok-build.md b/docs-site/src/content/docs/ru/guides/grok-build.md index bd8ac09d7c..084aa9c94e 100644 --- a/docs-site/src/content/docs/ru/guides/grok-build.md +++ b/docs-site/src/content/docs/ru/guides/grok-build.md @@ -3,7 +3,7 @@ title: Grok Build description: Используйте любую модель, маршрутизируемую opencodex, из CLI xAI Grok Build — пока прокси работает, модели автоматически регистрируются в ~/.grok/config.toml. --- -opencodex отдаёт OpenAI-совместимый `POST /v1/chat/completions` (и `/v1/responses`) на своём +opencodex отдаёт OpenAI-совместимый `POST /v1/responses` на своём локальном порту, а Grok Build поддерживает custom-модели поверх OpenAI-совместимых серверов. Начиная с этой интеграции, opencodex автоматически регистрирует весь свой видимый каталог в Grok Build — вручную редактировать конфигурацию не нужно. diff --git a/docs-site/src/content/docs/tr/guides/grok-build.md b/docs-site/src/content/docs/tr/guides/grok-build.md index 94b669874e..28e851c7a9 100644 --- a/docs-site/src/content/docs/tr/guides/grok-build.md +++ b/docs-site/src/content/docs/tr/guides/grok-build.md @@ -3,8 +3,7 @@ title: Grok Build description: xAI Grok Build CLI içerisinden opencodex ile yönlendirilen herhangi bir modeli kullanın — proxy çalışırken modeller ~/.grok/config.toml içine otomatik olarak kaydedilir. --- -opencodex, yerel portunda OpenAI uyumlu bir `POST /v1/chat/completions` (ve -`/v1/responses`) sunar ve Grok Build, OpenAI uyumlu sunuculara karşı özel +opencodex, yerel portunda OpenAI uyumlu bir `POST /v1/responses` sunar ve Grok Build, OpenAI uyumlu sunuculara karşı özel modelleri destekler. Bu entegrasyonla başlayarak opencodex, görünür kataloğunun tamamını otomatik olarak Grok Build'e kaydeder — manuel yapılandırma düzenlemesi gerekmez. @@ -159,4 +158,3 @@ adlar bu nedenle noktalardan tamamen kaçınır. - **Katalog güncellemeleri:** çitle çevrili blok, enjeksiyon anındaki kataloğu yansıtır. Sağlayıcılar veya modeller ekledikten sonra yenilemek için `ocx ensure` çalıştırın (veya proxy'yi yeniden başlatın). - diff --git a/docs-site/src/content/docs/zh-cn/guides/grok-build.md b/docs-site/src/content/docs/zh-cn/guides/grok-build.md index 766e8f81b1..e239aafb1e 100644 --- a/docs-site/src/content/docs/zh-cn/guides/grok-build.md +++ b/docs-site/src/content/docs/zh-cn/guides/grok-build.md @@ -3,7 +3,7 @@ title: Grok Build description: 在 xAI 的 Grok Build CLI 中使用任何由 opencodex 路由的模型——在代理运行期间,模型会自动注册到 ~/.grok/config.toml。 --- -opencodex 在本地端口提供一个与 OpenAI 兼容的 `POST /v1/chat/completions`(以及 `/v1/responses`),而 Grok Build 支持针对与 OpenAI 兼容的服务器使用自定义模型。从这次集成开始,opencodex 会将其全部可见目录自动注册到 Grok Build 中,无需手动编辑配置。 +opencodex 在本地端口提供一个与 OpenAI 兼容的 `POST /v1/responses`,而 Grok Build 支持针对与 OpenAI 兼容的服务器使用自定义模型。从这次集成开始,opencodex 会将其全部可见目录自动注册到 Grok Build 中,无需手动编辑配置。 ## 自动注册 diff --git a/docs-site/src/content/docs/zh-tw/guides/grok-build.md b/docs-site/src/content/docs/zh-tw/guides/grok-build.md index 364f92c3fd..30efd484c9 100644 --- a/docs-site/src/content/docs/zh-tw/guides/grok-build.md +++ b/docs-site/src/content/docs/zh-tw/guides/grok-build.md @@ -3,7 +3,7 @@ title: Grok Build description: 透過 xAI 的 Grok Build CLI 使用任何由 opencodex 路由的模型——代理程式執行期間會將模型自動註冊到 ~/.grok/config.toml。 --- -opencodex 在本機埠提供 OpenAI 相容的 `POST /v1/chat/completions`(以及 `/v1/responses`),而 Grok Build 支援對 OpenAI 相容伺服器使用自訂模型。從此整合開始,opencodex 會自動將其整個可見目錄註冊到 Grok Build——無需手動編輯設定。 +opencodex 在本機埠提供 OpenAI 相容的 `POST /v1/responses`,而 Grok Build 支援對 OpenAI 相容伺服器使用自訂模型。從此整合開始,opencodex 會自動將其整個可見目錄註冊到 Grok Build——無需手動編輯設定。 ## 自動註冊