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
4 changes: 4 additions & 0 deletions docs-site/src/content/docs/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ openai_base_url = "http://127.0.0.1:10100/v1"
fast_mode = true
```

The injected `fast_mode` follows the tri-state `fastMode` setting: `true` writes `fast_mode = true`,
`false` writes `fast_mode = false`, and unset leaves an existing `fast_mode` untouched without
adding a `[features]` table.

The proxy listens on port `10100` by default and serves `POST /v1/responses`,
`POST /v1/responses/compact`, `POST /v1/images/generations`, `POST /v1/images/edits`,
`GET /v1/models`, `GET /healthz`, and the `/api/*` management surface.
Expand Down
4 changes: 4 additions & 0 deletions docs-site/src/content/docs/ja/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ openai_base_url = "http://127.0.0.1:10100/v1"
fast_mode = true
```

注入される `fast_mode` は三値の `fastMode` 設定に従います。`true` は `fast_mode = true` を書き込み、
`false` は `fast_mode = false` を書き込み、未設定の場合は既存の `fast_mode` を変更せずに
`[features]` テーブルも追加しません。

プロキシはデフォルトでポート `10100` をリッスンし、`POST /v1/responses`、`POST /v1/responses/compact`、`POST /v1/images/generations`、`POST /v1/images/edits`、`GET /v1/models`、`GET /healthz`、および `/api/*` 管理サーフェスを提供します。

### 組み込みの画像生成 (`image_gen`)
Expand Down
4 changes: 4 additions & 0 deletions docs-site/src/content/docs/ko/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ openai_base_url = "http://127.0.0.1:10100/v1"
fast_mode = true
```

주입되는 `fast_mode`는 3-상태 `fastMode` 설정을 따릅니다. `true`면 `fast_mode = true`를 쓰고,
`false`면 `fast_mode = false`를 쓰며, 설정하지 않으면 기존 `fast_mode`를 그대로 두고
`[features]` 테이블도 추가하지 않습니다.

프록시는 기본적으로 포트 `10100`에서 듣고 `POST /v1/responses`, `POST /v1/responses/compact`, `POST /v1/images/generations`, `POST /v1/images/edits`, `GET /v1/models`, `GET /healthz`, 그리고 `/api/*` 관리 표면을 제공합니다.

### 내장 이미지 생성 (`image_gen`)
Expand Down
4 changes: 4 additions & 0 deletions docs-site/src/content/docs/ru/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ openai_base_url = "http://127.0.0.1:10100/v1"
fast_mode = true
```

Инжектируемый `fast_mode` следует трёхзначной настройке `fastMode`: `true` записывает
`fast_mode = true`, `false` — `fast_mode = false`, а при отсутствии настройки существующий
`fast_mode` сохраняется без изменений, и таблица `[features]` не добавляется.

Прокси по умолчанию слушает порт `10100` и обслуживает `POST /v1/responses`,
`POST /v1/responses/compact`, `POST /v1/images/generations`, `POST /v1/images/edits`,
`GET /v1/models`, `GET /healthz` и management surface `/api/*`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ openai_base_url = "http://127.0.0.1:10100/v1"
fast_mode = true
```

注入的 `fast_mode` 遵循三态 `fastMode` 配置:`true` 写入 `fast_mode = true`,`false` 写入
`fast_mode = false`,未设置时保留用户已有的 `fast_mode` 且不添加 `[features]` 表。

proxy 默认监听 `10100` 端口,并提供 `POST /v1/responses`、`POST /v1/responses/compact`、
`POST /v1/images/generations`、`POST /v1/images/edits`、`GET /v1/models`、`GET /healthz`,
以及 `/api/*` 管理面。
Expand Down
32 changes: 21 additions & 11 deletions src/codex/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,25 +397,35 @@ function normalizeServiceTier(content: string): string {
return content.replace(/^(\s*service_tier\s*=\s*)["']priority["']\s*$/gm, '$1"fast"');
}

function ensureFastModeFeature(content: string): string {
function ensureFastModeFeature(content: string, fastMode?: boolean): string {
// Tri-state fast mode (see OcxConfig.fastMode): true forces `fast_mode = true`,
// false forces `fast_mode = false`, and undefined leaves the user's config
// untouched (no [features] table is added and an existing fast_mode line is
// preserved as-is). Table and key matching accept the valid TOML spellings
// `[features] # comment`, `["features"]` / `['features']`, and quoted keys.
const lines = content.split("\n");
const featuresStart = lines.findIndex(line => line.trim() === "[features]");
const featuresHeader = /^\s*\[(["']?)\s*features\s*\1\]\s*(?:#.*)?$/;
const fastModeKey = /^\s*(?:"fast_mode"|'fast_mode'|fast_mode)\s*=/;
const featuresStart = lines.findIndex(line => featuresHeader.test(line));
if (featuresStart === -1) {
return content.trimEnd() + "\n\n[features]\nfast_mode = true\n";
if (fastMode === undefined) return content;
return content.trimEnd() + "\n\n[features]\nfast_mode = " + (fastMode ? "true" : "false") + "\n";
}

const nextTable = lines.findIndex((line, index) => index > featuresStart && /^\s*\[/.test(line));
const featuresEnd = nextTable === -1 ? lines.length : nextTable;
for (let i = featuresStart + 1; i < featuresEnd; i++) {
if (/^\s*fast_mode\s*=/.test(lines[i])) {
lines[i] = lines[i].replace(/^(\s*)fast_mode\s*=.*$/, "$1fast_mode = true");
if (fastModeKey.test(lines[i])) {
if (fastMode === undefined) return lines.join("\n");
lines[i] = lines[i].replace(/^(\s*)(?:"fast_mode"|'fast_mode'|fast_mode)\s*=.*$/, `$1fast_mode = ${fastMode ? "true" : "false"}`);
return lines.join("\n");
}
}

if (fastMode === undefined) return lines.join("\n");
let insertAt = featuresEnd;
while (insertAt > featuresStart + 1 && lines[insertAt - 1].trim() === "") insertAt--;
lines.splice(insertAt, 0, "fast_mode = true");
lines.splice(insertAt, 0, `fast_mode = ${fastMode ? "true" : "false"}`);
return lines.join("\n");
}

Expand All @@ -433,7 +443,7 @@ function stripOpencodexCatalogPath(content: string): string {
.join("\n");
}

export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets = false, includeApiAuthHeader = false, hostname?: string): string {
export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets = false, includeApiAuthHeader = false, hostname?: string, fastMode?: boolean): string {
const host = providerBaseHost(hostname);
// Design B (loopback): the reference/fallback file documents the root override form.
// Non-loopback keeps the legacy provider-table shape (built-in provider cannot carry
Expand All @@ -446,7 +456,7 @@ export function buildProfileFile(port: number, catalogPath?: string | null, supp
buildOpenaiBaseUrlLine(port, hostname),
];
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
lines.push("", "[features]", "fast_mode = true", "");
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`, "");
return lines.join("\n");
}
const lines = [
Expand All @@ -455,7 +465,7 @@ export function buildProfileFile(port: number, catalogPath?: string | null, supp
'model_provider = "opencodex"',
];
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
lines.push("", "[features]", "fast_mode = true");
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`);
lines.push(buildProviderTableBlock(port, supportsWebsockets, includeApiAuthHeader, hostname).trimEnd(), "");
return lines.join("\n");
}
Expand Down Expand Up @@ -542,7 +552,7 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
content = stripExistingModelProvider(content);
content = stripRootContextWindowOverrides(content);
content = normalizeServiceTier(content);
content = ensureFastModeFeature(content);
content = ensureFastModeFeature(content, config?.fastMode);

const catalogPath = chooseCatalogPathForInjection(content, options.catalogPath);
content = catalogPath ? setRootModelCatalogPath(content, catalogPath) : stripOpencodexCatalogPath(content);
Expand Down Expand Up @@ -590,7 +600,7 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
}

const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}), legacyMode, config?.hostname);
const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}), legacyMode, config?.hostname, config?.fastMode);
content = applyEol(content, eol);
atomicWriteFile(CODEX_CONFIG_PATH, content);
atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
Expand Down
119 changes: 119 additions & 0 deletions tests/codex-inject-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,125 @@ describe("injectCodexConfig integration (Design B)", () => {
expect(second).toBe(first);
});

test("fastMode=false forces fast_mode=false in both config and profile", () => {
writeFileSync(join(codexHome, "config.toml"), 'model = "gpt-5.5"\n', "utf8");

const r = runInject(codexHome, ocxHome, JSON.stringify({ fastMode: false }));
expect(r.status).toBe(0);
expect(JSON.parse(r.stdout).success).toBe(true);

const config = readFileSync(join(codexHome, "config.toml"), "utf8");
expect(config).toContain("[features]");
expect(config).toContain("fast_mode = false");
expect(config).not.toContain("fast_mode = true");

const profile = readFileSync(join(codexHome, "opencodex.config.toml"), "utf8");
expect(profile).toContain("fast_mode = false");
expect(profile).not.toContain("fast_mode = true");
});

test("fastMode=true adds fast_mode=true to a config without a [features] table", () => {
writeFileSync(join(codexHome, "config.toml"), 'model = "gpt-5.5"\n', "utf8");

const r = runInject(codexHome, ocxHome, JSON.stringify({ fastMode: true }));
expect(r.status).toBe(0);
expect(JSON.parse(r.stdout).success).toBe(true);

const config = readFileSync(join(codexHome, "config.toml"), "utf8");
expect(config).toContain("[features]");
expect(config).toContain("fast_mode = true");

const profile = readFileSync(join(codexHome, "opencodex.config.toml"), "utf8");
expect(profile).toContain("fast_mode = true");
});

test("fastMode unset preserves the user's existing fast_mode setting", () => {
writeFileSync(join(codexHome, "config.toml"), 'model = "gpt-5.5"\n\n[features]\nfast_mode = false\n', "utf8");

const r = runInject(codexHome, ocxHome);
expect(r.status).toBe(0);
expect(JSON.parse(r.stdout).success).toBe(true);

const config = readFileSync(join(codexHome, "config.toml"), "utf8");
expect(config).toContain("fast_mode = false");
expect(config).not.toContain("fast_mode = true");

const profile = readFileSync(join(codexHome, "opencodex.config.toml"), "utf8");
expect(profile).not.toContain("fast_mode");
});

test("fastMode unset does not add a [features] table to a config that lacks one", () => {
writeFileSync(join(codexHome, "config.toml"), 'model = "gpt-5.5"\n', "utf8");

const r = runInject(codexHome, ocxHome);
expect(r.status).toBe(0);
expect(JSON.parse(r.stdout).success).toBe(true);

const config = readFileSync(join(codexHome, "config.toml"), "utf8");
expect(config).not.toContain("[features]");
expect(config).not.toContain("fast_mode");

const profile = readFileSync(join(codexHome, "opencodex.config.toml"), "utf8");
expect(profile).not.toContain("fast_mode");
});

test("fastMode=false updates a commented [features] header without duplicating the table", () => {
writeFileSync(join(codexHome, "config.toml"), [
'model = "gpt-5.5"',
"",
"[features] # user comment",
"fast_mode = true",
"",
].join("\n"), "utf8");

const r = runInject(codexHome, ocxHome, JSON.stringify({ fastMode: false }));
expect(r.status).toBe(0);

const config = readFileSync(join(codexHome, "config.toml"), "utf8");
expect(config).toContain("fast_mode = false");
expect(config).not.toContain("fast_mode = true");
expect(() => Bun.TOML.parse(config)).not.toThrow();
expect(Bun.TOML.parse(config).features.fast_mode).toBe(false);
});

test("fastMode=false updates a quoted [\"features\"] header without duplicating the table", () => {
writeFileSync(join(codexHome, "config.toml"), [
'model = "gpt-5.5"',
"",
'["features"]',
"fast_mode = true",
"",
].join("\n"), "utf8");

const r = runInject(codexHome, ocxHome, JSON.stringify({ fastMode: false }));
expect(r.status).toBe(0);

const config = readFileSync(join(codexHome, "config.toml"), "utf8");
expect(config).toContain("fast_mode = false");
expect(config).not.toContain("fast_mode = true");
expect(() => Bun.TOML.parse(config)).not.toThrow();
expect(Bun.TOML.parse(config).features.fast_mode).toBe(false);
});

test("fastMode=false updates a quoted \"fast_mode\" key", () => {
writeFileSync(join(codexHome, "config.toml"), [
'model = "gpt-5.5"',
"",
"[features]",
'"fast_mode" = true',
"",
].join("\n"), "utf8");

const r = runInject(codexHome, ocxHome, JSON.stringify({ fastMode: false }));
expect(r.status).toBe(0);

const config = readFileSync(join(codexHome, "config.toml"), "utf8");
expect(config).toContain("fast_mode = false");
expect(config).not.toContain("fast_mode = true");
expect(() => Bun.TOML.parse(config)).not.toThrow();
expect(Bun.TOML.parse(config).features.fast_mode).toBe(false);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test("opt-in injects native subagent defaults, removes them when disabled, and restores the native config", () => {
const original = [
'model = "gpt-5.5"',
Expand Down
24 changes: 23 additions & 1 deletion tests/codex-inject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,29 @@ describe("Codex config injection", () => {
expect(profile).not.toContain('model_provider = "opencodex"');
expect(profile).not.toContain("[model_providers.opencodex]");
expect(profile).not.toContain("model_catalog_json");
expect(profile).toContain("fast_mode = true");
});

test("fallback profile does not force fast_mode when fastMode is unset", () => {
expect(buildProfileFile(10100, null)).not.toContain("fast_mode");
expect(buildProfileFile(10100, null, false, true, "192.168.1.20")).not.toContain("fast_mode");
});

test("fallback profile mirrors an explicit fastMode=true override", () => {
const loopback = buildProfileFile(10100, null, false, false, undefined, true);

expect(loopback).toContain("fast_mode = true");
expect(loopback).not.toContain("fast_mode = false");
});

test("fallback profile mirrors an explicit fastMode=false override", () => {
const loopback = buildProfileFile(10100, null, false, false, undefined, false);

expect(loopback).toContain("fast_mode = false");
expect(loopback).not.toContain("fast_mode = true");

const legacy = buildProfileFile(10100, null, false, true, "192.168.1.20", false);
expect(legacy).toContain("fast_mode = false");
expect(legacy).not.toContain("fast_mode = true");
});

test("non-loopback fallback profile keeps the legacy provider-table shape with the injected host", () => {
Expand Down
Loading