Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions docs-site/src/content/docs/fr/guides/grok-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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

Expand Down
21 changes: 10 additions & 11 deletions docs-site/src/content/docs/guides/grok-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 ...
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/ja/guides/grok-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 に自動的に登録します。手動による構成編集は必要ありません。

## 自動登録

Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/ko/guides/grok-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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에 자동 등록합니다. 수동으로 설정 파일을 편집할 필요가 없습니다.

## 자동 등록

Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/ru/guides/grok-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 — вручную редактировать конфигурацию не нужно.
Expand Down
4 changes: 1 addition & 3 deletions docs-site/src/content/docs/tr/guides/grok-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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).

2 changes: 1 addition & 1 deletion docs-site/src/content/docs/zh-cn/guides/grok-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 中,无需手动编辑配置。

## 自动注册

Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/zh-tw/guides/grok-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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——無需手動編輯設定。

## 自動註冊

Expand Down
42 changes: 36 additions & 6 deletions src/server/responses/responses-field-backfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,32 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
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<Record<string, string>> = {
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<string, unknown>, outputIndex: number): Record<string, unknown> {
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.
Expand Down Expand Up @@ -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 }) };
}

/**
Expand All @@ -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;
});
Expand All @@ -100,7 +128,9 @@ function rewriteEvent(event: Record<string, unknown>): Record<string, unknown> {
// 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;
Expand Down
140 changes: 140 additions & 0 deletions tests/responses-field-backfill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Comment thread
bet4it marked this conversation as resolved.
});

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");
});
});
Loading