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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ description: リスナー、リモート アクセス、アドミッション
| `hostname?` | `string` | `"127.0.0.1"` |バインドアドレス。非ループバック バインドには `OPENCODEX_API_AUTH_TOKEN` が必要です。 |
| `proxy?` | `string` | — |送信 HTTP(S) プロキシ URL または `${ENV_VAR}`。これらの変数が設定されていない場合にのみ、`HTTP_PROXY` / `HTTPS_PROXY` に適用されます。ループバックは `NO_PROXY` に残ります。 |
| `emptyCompletionRetry?` | `boolean` | `false` | テキストもツール呼び出しもない Responses ターンを、ターミナルイベント前にストリームが終了した場合も含め、同一リクエストで 1 回再試行するよう明示的に有効化します。再試行は課金対象になる場合があります。`OCX_EMPTY_COMPLETION_RETRY=0` で設定を変更せず無効化できます。combo と routed-compaction turn は対象外です。 |
| `dropCodexSafetyBuffering?` | `boolean` | `false` | Codex Responses パススルーから Codex の safety-buffering ヒントを除去します。対象は `x-codex-safety-buffering-enabled` / `x-codex-safety-buffering-faster-model` 応答ヘッダー、`safety_buffering` 型の `response.metadata` SSE イベント、およびその他の SSE イベントにある `safety_buffering` フィールドです。Codex TUI はこれらを、既定の操作でセッションをより弱いモデルに切り替える「より高速なモデルで再試行」プロンプトとして表示します。その他の `x-codex-*` ヘッダーと SSE イベントの内容は、そのフィールドの除去を除いて変更せずに転送されます。既定ではオフです。 |
| `stallTimeoutSec?` | `number` | `300` | `response.incomplete` より前にアップストリーム データがない秒数。最小 1。
| `connectTimeoutMs?` | `number` | `200000` |試行ごとの DNS/TCP/TLS/最終ヘッダーの期限。本体が生成される前に終了します。 |
| `shutdownTimeoutMs?` | `number` | `5000` |アクティブなターンが中止される前の正常な排出期限。 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ description: 리스너, 원격 접근, admission 키, 타임아웃, 저장소,
| `hostname?` | `string` | `"127.0.0.1"` | 바인드 주소입니다. 루프백이 아닌 바인드에는 `OPENCODEX_API_AUTH_TOKEN`이 필요합니다. |
| `proxy?` | `string` | — | 송신용 HTTP(S) 프록시 URL 또는 `${ENV_VAR}`입니다. 해당 변수가 비어 있을 때만 `HTTP_PROXY` / `HTTPS_PROXY`에 적용되며, 루프백은 `NO_PROXY`에 그대로 남습니다. |
| `emptyCompletionRetry?` | `boolean` | `false` | 텍스트나 도구 호출이 없는 Responses 턴을, 터미널 이벤트 전에 스트림이 종료된 경우를 포함해 동일한 요청으로 한 번 재시도하도록 선택합니다. 재시도에는 비용이 발생할 수 있습니다. `OCX_EMPTY_COMPLETION_RETRY=0`은 설정을 바꾸지 않고 비활성화하며, combo 및 routed-compaction turn은 제외됩니다. |
| `dropCodexSafetyBuffering?` | `boolean` | `false` | Codex Responses 패스스루에서 Codex safety-buffering 힌트를 제거합니다. 대상은 `x-codex-safety-buffering-enabled` / `x-codex-safety-buffering-faster-model` 응답 헤더, `safety_buffering` 형식의 `response.metadata` SSE 이벤트, 다른 SSE 이벤트의 `safety_buffering` 필드입니다. Codex TUI는 이 힌트를 기본 동작이 세션을 더 약한 모델로 전환하는 “더 빠른 모델로 재시도” 프롬프트로 표시합니다. 다른 `x-codex-*` 헤더와 다른 모든 SSE 이벤트 내용은 해당 필드 제거를 제외하고 그대로 전달됩니다. 기본적으로 꺼져 있습니다. |
| `stallTimeoutSec?` | `number` | `300` | 업스트림 데이터가 없을 때 `response.incomplete`가 되기까지의 초 수입니다. 최소 1입니다. |
| `connectTimeoutMs?` | `number` | `200000` | 시도별 DNS/TCP/TLS/최종 헤더 기한입니다. 본문 생성 전에 끝납니다. |
| `shutdownTimeoutMs?` | `number` | `5000` | 진행 중인 turn을 중단하기 전에 허용하는 정상 종료 드레인 기한입니다. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ runs helper features around provider requests.
| `proxy?` | `string` | — | Outbound HTTP(S) proxy URL, `${ENV_VAR}`, or `"auto"`. Applied to `HTTP_PROXY` / `HTTPS_PROXY` only when those variables are unset; loopback remains in `NO_PROXY`. `"auto"` reads the Windows system proxy (WinINET `ProxyEnable`/`ProxyServer`, `https=` then `http=` entry) once at process start and logs the host it chose. On other platforms, or when the system proxy is off, SOCKS-only, or unreadable, it uses direct egress and says so. PAC/WPAD and live proxy changes are not followed; restart the service after changing the system proxy. |
| `noProxy?` | `string \| string[]` | — | Hosts that bypass `proxy`, merged with inherited `NO_PROXY` and loopback entries. A string may use comma-separated `NO_PROXY` syntax or `${ENV_VAR}`. |
| `emptyCompletionRetry?` | `boolean` | `false` | Opt in to one identical Responses retry when a turn has no text or tool call, including a stream that ends before a terminal event. The retry may be billable. `OCX_EMPTY_COMPLETION_RETRY=0` disables it without changing config; combo and routed-compaction turns remain excluded. |
| `dropCodexSafetyBuffering?` | `boolean` | `false` | Strip the Codex safety-buffering hints from Codex Responses passthroughs: the `x-codex-safety-buffering-enabled` / `x-codex-safety-buffering-faster-model` response headers, `response.metadata` SSE events of type `safety_buffering`, and the `safety_buffering` field on other SSE events. The Codex TUI renders those hints as a "retry with a faster model" prompt whose default action switches the session to the weaker model, which an unattended session can trigger by accident. Other `x-codex-*` headers and all other SSE event content pass through unchanged except for removal of that field. Off by default. |
| `stallTimeoutSec?` | `number` | `300` | Seconds without upstream data before `response.incomplete`. Minimum 1. |
| `oauthOpenBrowser?` | `boolean` | `true` | Whether a login may open a browser on the machine running the proxy. Absent and `true` both open, so an existing install is unchanged; only an explicit `false` declines. Decline when you need the authorization link in a different browser profile, or when the dashboard is not on the proxy's machine — the login still starts and the URL is still returned and displayed. `POST /api/oauth/login` and `POST /api/codex-auth/login` accept a per-request `openBrowser` boolean that overrides this, and the dashboard exposes the same choice beside the login button. Device-code flows never open a browser either way. |
| `connectTimeoutMs?` | `number` | `200000` | Per-attempt DNS/TCP/TLS/final-header deadline; it ends before body generation. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ description: Listener, удалённый доступ, admission key, тайм
| `hostname?` | `string` | `"127.0.0.1"` | Адрес bind'а. Не-loopback bind требует `OPENCODEX_API_AUTH_TOKEN`. |
| `proxy?` | `string` | — | URL исходящего HTTP(S)-прокси или `${ENV_VAR}`. Применяется к `HTTP_PROXY` / `HTTPS_PROXY` только когда эти переменные не заданы; loopback всегда остаётся в `NO_PROXY`. |
| `emptyCompletionRetry?` | `boolean` | `false` | Явно включает один идентичный повтор Responses, если в turn нет ни текста, ни tool call, включая случай, когда stream завершается до terminal event. Повтор может тарифицироваться. `OCX_EMPTY_COMPLETION_RETRY=0` отключает его без изменения config; combo и routed-compaction turn исключены. |
| `dropCodexSafetyBuffering?` | `boolean` | `false` | Удаляет подсказки Codex safety-buffering из passthrough-ответов Codex Responses: заголовки `x-codex-safety-buffering-enabled` / `x-codex-safety-buffering-faster-model`, SSE-события `response.metadata` типа `safety_buffering` и поле `safety_buffering` в других SSE-событиях. Codex TUI отображает их как предложение повторить запрос с более быстрой моделью, действие по умолчанию в котором переключает сессию на более слабую модель. Остальные заголовки `x-codex-*` и содержимое других SSE-событий передаются без изменений, кроме удаления этого поля. По умолчанию выключено. |
| `stallTimeoutSec?` | `number` | `300` | Секунды без upstream-данных до `response.incomplete`. Минимум 1. |
| `connectTimeoutMs?` | `number` | `200000` | Дедлайн одной попытки DNS/TCP/TLS/final-header; он завершается до генерации тела ответа. |
| `shutdownTimeoutMs?` | `number` | `5000` | Дедлайн graceful-drain до принудительного прерывания активных turn'ов. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ description: 监听、远程访问、准入密钥、超时、存储、侧车、
| `hostname?` | `string` | `"127.0.0.1"` | 绑定地址。非回环绑定需要 `OPENCODEX_API_AUTH_TOKEN`。 |
| `proxy?` | `string` | — | 出站 HTTP(S) 代理 URL,或 `${ENV_VAR}`。仅当 `HTTP_PROXY` / `HTTPS_PROXY` 未设置时才会应用;回环地址始终保留在 `NO_PROXY` 中。 |
| `emptyCompletionRetry?` | `boolean` | `false` | 显式启用:当 Responses turn 既无文本也无工具调用时,使用相同请求重试一次,包括流在终止事件之前结束的情况。重试可能产生费用。`OCX_EMPTY_COMPLETION_RETRY=0` 可在不修改配置的情况下禁用;combo 与 routed-compaction turn 不参与。 |
| `dropCodexSafetyBuffering?` | `boolean` | `false` | 从 Codex Responses 透传响应中移除 Codex safety-buffering 提示:`x-codex-safety-buffering-enabled` / `x-codex-safety-buffering-faster-model` 响应头、类型为 `safety_buffering` 的 `response.metadata` SSE 事件,以及其他 SSE 事件中的 `safety_buffering` 字段。Codex TUI 会将这些提示显示为“使用更快模型重试”的提示框,其默认操作会把会话切换到较弱的模型。其他 `x-codex-*` 响应头和其他所有 SSE 事件内容均保持不变,但会移除该字段。默认关闭。 |
| `stallTimeoutSec?` | `number` | `300` | 在上游没有数据之前可等待的秒数,超过后返回 `response.incomplete`。最小值为 1。 |
| `connectTimeoutMs?` | `number` | `200000` | 每次尝试的 DNS/TCP/TLS/最终响应头截止时间;它在正文生成之前结束。 |
| `shutdownTimeoutMs?` | `number` | `5000` | 优雅停机截止时间,超过后会中止仍在进行中的请求。 |
Expand Down
12 changes: 12 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,8 @@ const configSchema = z.object({
configRebaseProvenance: z.unknown().optional(),
// A retry can be billable, so absence and malformed hand edits both stay off.
emptyCompletionRetry: z.boolean().optional().catch(false),
// Header suppression changes what Codex sees, so absence and malformed edits stay off.
dropCodexSafetyBuffering: z.boolean().optional().catch(false),
// A malformed hand edit must not silently stop opening the browser: fall back
// to undefined, which resolves to the historical auto-open behavior.
oauthOpenBrowser: z.boolean().optional().catch(undefined),
Expand Down Expand Up @@ -2613,6 +2615,14 @@ function emptyCompletionRetryError(value: unknown): string | null {
return "schema_invalid: emptyCompletionRetry: must be a boolean or omitted";
}

function dropCodexSafetyBufferingError(value: unknown): string | null {
const raw = rawConfigRecord(value);
if (!raw || !Object.hasOwn(raw, "dropCodexSafetyBuffering")) return null;
const enabled = raw.dropCodexSafetyBuffering;
if (enabled === undefined || typeof enabled === "boolean") return null;
return "schema_invalid: dropCodexSafetyBuffering: must be a boolean or omitted";
}

function oauthOpenBrowserError(value: unknown): string | null {
const raw = rawConfigRecord(value);
if (!raw || !Object.hasOwn(raw, "oauthOpenBrowser")) return null;
Expand Down Expand Up @@ -2718,6 +2728,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
?? codexQuotaAutoRefreshError(value)
?? codexAccountPickerEnabledError(value)
?? emptyCompletionRetryError(value)
?? dropCodexSafetyBufferingError(value)
?? oauthOpenBrowserError(value)
?? runtimeRoleError(value)
?? remoteGuiConfigError(value)
Expand Down Expand Up @@ -3684,6 +3695,7 @@ export function getDefaultConfig(): OcxConfig {
return {
port: 10100,
emptyCompletionRetry: false,
dropCodexSafetyBuffering: false,
managementUsageMaxReadBytes: 64 * 1024 * 1024,
appOwnedMemoryBudgetMb: DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES / (1024 * 1024),
// Fresh/re-initialized configs are already written in the current three-tier
Expand Down
1 change: 1 addition & 0 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ import {
} from "./relay";
export {
consumeForInspection,
codexSafetyBufferingFilterOptions,
relaySseWithFailedTail,
relaySseWithHeartbeat,
relayWithAbort,
Expand Down
5 changes: 4 additions & 1 deletion src/server/relay-eager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

import {
adapterEofIncompleteFrame,
type CodexSafetyBufferingFilterOptions,
createSseTerminalOutputBoundary,
doneFrame,
failedTailFrame,
Expand Down Expand Up @@ -83,6 +84,8 @@ export type EagerRelayOptions = {
postCancelDrainBytes?: number;
/** Injectable clock for tests. */
now?: () => number;
/** Client output boundary filters (Codex safety-buffering hints). */
terminalBoundary?: CodexSafetyBufferingFilterOptions;
};

const DEFAULT_MAX_QUEUE_BYTES = 8 * 1024 * 1024;
Expand Down Expand Up @@ -111,7 +114,7 @@ export function relaySseEagerBounded(
const terminalEncoder = new TextEncoder();
const adapterEofFrame = adapterEofIncompleteFrame(terminalEncoder);
const terminalSentinel = doneFrame(terminalEncoder);
const terminalBoundary = createSseTerminalOutputBoundary();
const terminalBoundary = createSseTerminalOutputBoundary(opts?.terminalBoundary);
const activeRewrite: SseBlockRewrite | undefined = hooks.rewriteBlocks
?? (hooks.rewritePayload ? payloadRewriteAsBlockRewrite(hooks.rewritePayload) : undefined);
const encodeFailedTail = (error: unknown): Uint8Array | null => {
Expand Down
70 changes: 65 additions & 5 deletions src/server/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,10 @@ export type SseTerminalOutputBoundary = {
* terminal, and drops every later block/byte. A premature [DONE] is held until
* a terminal arrives so clean EOF can synthesize one terminal and one sentinel.
*/
export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
export function createSseTerminalOutputBoundary(
options?: CodexSafetyBufferingFilterOptions,
): SseTerminalOutputBoundary {
const dropSafetyBuffering = options?.dropCodexSafetyBuffering === true;
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const framer = new BoundedSseFrameBuffer(MAX_INSPECTION_SSE_FRAME_BYTES);
Expand All @@ -181,6 +184,10 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
const payload = sseDataPayload(decoder.decode(frame.block));
const isDone = payload === "[DONE]";
const parsed = payload === null ? undefined : parseSsePayload(payload);
const safetyBuffering = dropSafetyBuffering && parsed !== undefined
? codexSafetyBufferingBlockAction(parsed)
: "keep";
if (safetyBuffering === "drop") continue;
const policyError = parsed !== undefined && isPolicyRewriteType(parsed)
? cyberPolicyTerminalError(parsed)
: undefined;
Expand All @@ -189,7 +196,9 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
decoder.decode(frame.block),
policyFailurePayload(policyError, parsed),
))
: frame.block;
: safetyBuffering === "strip"
? encoder.encode(stripCodexSafetyBufferingField(decoder.decode(frame.block), parsed))
: frame.block;
if (isDone) {
done = true;
if (responsesTerminal) {
Expand Down Expand Up @@ -260,10 +269,11 @@ export function relaySseWithFailedTail(
body: ReadableStream<Uint8Array>,
upstream: AbortController,
onClientGone?: (reason?: unknown) => void,
boundaryOptions?: CodexSafetyBufferingFilterOptions,
): ReadableStream<Uint8Array> {
const reader = body.getReader();
const encoder = new TextEncoder();
const terminalBoundary = createSseTerminalOutputBoundary();
const terminalBoundary = createSseTerminalOutputBoundary(boundaryOptions);
let closed = false;
const relayChunk = (
controller: ReadableStreamDefaultController<Uint8Array>,
Expand Down Expand Up @@ -438,6 +448,29 @@ function isPolicyRewriteType(parsed: unknown): boolean {
return type === "response.failed" || type === "response.incomplete" || type === "error";
}

/**
* Codex emits its safety-buffering hint in the SSE body as well as in headers:
* a `response.metadata` event whose `metadata.type` is `safety_buffering`, or a
* `safety_buffering` field on another event. The metadata event is dropped whole;
* the field is stripped so the carrying event is otherwise relayed unchanged.
*/
function codexSafetyBufferingBlockAction(parsed: unknown): "keep" | "drop" | "strip" {
const root = asJsonRecord(parsed);
if (!root) return "keep";
if (root.type === "response.metadata") {
const metadata = asJsonRecord(root.metadata);
if (metadata?.type === "safety_buffering") return "drop";
}
return Object.hasOwn(root, "safety_buffering") ? "strip" : "keep";
}

function stripCodexSafetyBufferingField(block: string, parsed: unknown): string {
const root = asJsonRecord(parsed);
if (!root) return block;
const { safety_buffering: _safetyBuffering, ...rest } = root;
return replaceSseDataPayload(block, JSON.stringify(rest));
}

function rewritePolicyTerminalBlock(block: string, payload: string): string {
const newline = block.includes("\r\n") ? "\r\n" : "\n";
const rewritten = replaceSseDataPayload(block, payload);
Expand Down Expand Up @@ -1422,7 +1455,31 @@ export function consumeForResponseLogMetadata(
* body makes the caller (Codex) double-decode / truncate → "stream error" on every gpt passthrough.
* Drop encoding + hop-by-hop headers; relay everything else (content-type, etc.) verbatim.
*/
export function sanitizePassthroughHeaders(upstream: Headers): Headers {
export const CODEX_SAFETY_BUFFERING_HEADERS = [
"x-codex-safety-buffering-enabled",
"x-codex-safety-buffering-faster-model",
] as const;

const CODEX_SAFETY_BUFFERING_HEADER_SET: ReadonlySet<string> = new Set(CODEX_SAFETY_BUFFERING_HEADERS);

export interface CodexSafetyBufferingFilterOptions {
/**
* Drop Codex safety-buffering hints: the `x-codex-safety-buffering-*` response
* headers and the `safety_buffering` SSE metadata event / field. Absent and
* `false` relay everything unchanged.
*/
dropCodexSafetyBuffering?: boolean;
}

/** Resolve the passthrough header policy from the loaded config (absent means "forward everything"). */
export function codexSafetyBufferingFilterOptions(
config: { dropCodexSafetyBuffering?: boolean },
): CodexSafetyBufferingFilterOptions {
return { dropCodexSafetyBuffering: config.dropCodexSafetyBuffering === true };
}

export function sanitizePassthroughHeaders(upstream: Headers, options?: CodexSafetyBufferingFilterOptions): Headers {
const dropSafetyBuffering = options?.dropCodexSafetyBuffering === true;
const DROP = new Set([
"content-encoding",
"content-length",
Expand All @@ -1439,7 +1496,10 @@ export function sanitizePassthroughHeaders(upstream: Headers): Headers {
]);
const out = new Headers();
upstream.forEach((value, key) => {
if (!DROP.has(key.toLowerCase())) out.set(key, value);
const lower = key.toLowerCase();
if (DROP.has(lower)) return;
if (dropSafetyBuffering && CODEX_SAFETY_BUFFERING_HEADER_SET.has(lower)) return;
out.set(key, value);
});
return out;
}
Loading
Loading