Skip to content
Merged
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
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
86 changes: 80 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,59 @@ 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_",
// The Responses wire type is `image_generation_call`; `image_gen_call` is kept only so a
// relay that emits the short spelling is not silently demoted to the generic `item_`.
image_generation_call: "ig_",
image_gen_call: "ig_",
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* 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>, slot: ItemIdSlot): 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_" + (slot.kind === "index" ? String(slot.index) : "fallback_" + slot.ordinal) };
}

/**
* Which namespace a synthesized id comes from.
*
* Keeping the fallback counter in the SAME numeric namespace as real output indexes only
* pushed the collision out of reach rather than removing it: a response whose real index
* happened to be 1_000_001 would produce the same id as the first malformed-index fallback,
* and a duplicate id is exactly what this backfill exists to prevent. The namespaces are now
* lexically disjoint, so no index value can ever collide with a fallback.
*/
type ItemIdSlot = { kind: "index"; index: number } | { kind: "fallback"; ordinal: number };

/**
* Monotonic ordinal for an event whose `output_index` is absent or malformed.
*
* Process-global rather than per-response because this module is stateless by design and the
* value only has to be unique, not meaningful. It carries its own `fallback_` namespace, so
* uniqueness no longer depends on a real index never reaching some arbitrary ceiling.
*/
let syntheticItemOrdinal = 0;
function nextSyntheticItemSlot(): ItemIdSlot {
syntheticItemOrdinal += 1;
return { kind: "fallback", ordinal: syntheticItemOrdinal };
}

/**
* Backfill annotations: [] on an output_text content part if missing.
* Returns the same object reference if no change is needed.
Expand Down Expand Up @@ -58,16 +111,29 @@ function backfillContentArray(content: unknown): unknown {
return changed ? repaired : content;
}

/**
* Item types that are NOT Responses output items and must be returned byte-for-byte.
*
* `compaction` is the `/v1/responses/compact` wire format, not a Responses output item. It has
* no `id` in that contract, so synthesizing one changes a response body the client compares
* exactly. The backfill exists to satisfy strict Responses decoders; a shape those decoders
* never see is outside its remit.
*/
const NON_RESPONSES_ITEM_TYPES: ReadonlySet<string> = new Set(["compaction"]);

/**
* 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, slot: ItemIdSlot): unknown {
if (!isPlainObject(item)) return item;
if (typeof item.type === "string" && NON_RESPONSES_ITEM_TYPES.has(item.type)) return item;
const content = item.content;
const repaired = backfillContentArray(content);
if (repaired === content) return item;
return { ...item, content: repaired };
const withId = backfillItemId(item, slot);
if (repaired === content && withId === item) return item;
return { ...withId, ...(repaired === content ? {} : { content: repaired }) };
}

/**
Expand All @@ -79,9 +145,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, { kind: "index", index: idx });
if (next !== item) changed = true;
return next;
});
Expand All @@ -100,7 +166,15 @@ 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;
// A malformed or absent `output_index` must not collapse to 0: two such events would then
// both synthesize `msg_ocx_0`, and duplicate ids are the very thing this backfill exists to
// prevent. Fall back to a per-process counter so the synthesized id stays unique. Position
// is not recoverable in that case, but a unique id is what strict decoders require, and a
// well-formed stream still gets the stable index-derived id.
const item = typeof rawIndex === "number" && Number.isInteger(rawIndex) && rawIndex >= 0
? backfillOutputItem(event.item, { kind: "index", index: rawIndex })
: backfillOutputItem(event.item, nextSyntheticItemSlot());
if (item !== event.item) {
next = { ...next, item };
changed = true;
Expand Down
Loading
Loading