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
47 changes: 42 additions & 5 deletions src/lib/configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ import TOML from "@iarna/toml";
import {
AI_GATEWAY_OPENAI_URL,
AI_GATEWAY_URL,
CLAUDE_AUTO_COMPACT_WINDOW,
CLAUDE_AUTOCOMPACT_PCT,
GATEWAY_COMPACT_PCT,
GATEWAY_COMPACT_RESERVED,
GATEWAY_COMPACT_TRIGGER,
GATEWAY_CONTEXT_WINDOW,
GATEWAY_MAX_OUTPUT_TOKENS,
} from "@/lib/const.js";
import { logInfo } from "@/lib/log.js";
import type { Agent } from "@/providers/types.js";
Expand Down Expand Up @@ -107,6 +110,8 @@ const CLAUDE_K = {
const CODEX_K = {
model: atob("bW9kZWw="),
modelProvider: atob("bW9kZWxfcHJvdmlkZXI="),
modelContextWindow: atob("bW9kZWxfY29udGV4dF93aW5kb3c="),
autoCompactTokenLimit: atob("bW9kZWxfYXV0b19jb21wYWN0X3Rva2VuX2xpbWl0"),
modelProviders: atob("bW9kZWxfcHJvdmlkZXJz"),
providerId: atob("YWlnYXRld2F5"),
name: atob("bmFtZQ=="),
Expand All @@ -133,6 +138,12 @@ const OPENCODE_K = {
baseURL: atob("YmFzZVVSTA=="),
apiKey: atob("YXBpS2V5"),
models: atob("bW9kZWxz"),
limit: atob("bGltaXQ="),
context: atob("Y29udGV4dA=="),
output: atob("b3V0cHV0"),
compaction: atob("Y29tcGFjdGlvbg=="),
auto: atob("YXV0bw=="),
reserved: atob("cmVzZXJ2ZWQ="),
};

// The base URL CoDev writes to each tool's config. Read back at export time so
Expand Down Expand Up @@ -515,8 +526,9 @@ export function configureClaudeCode(creds: Credentials): ConfigureResult[] {
[CLAUDE_K.sonnet]: model,
[CLAUDE_K.haiku]: model,
[CLAUDE_K.agentTeams]: "1",
[CLAUDE_K.autoCompactWindow]: CLAUDE_AUTO_COMPACT_WINDOW,
[CLAUDE_K.autoCompactPct]: CLAUDE_AUTOCOMPACT_PCT,
// Env-var values are strings; the shared window/percentage are numeric.
[CLAUDE_K.autoCompactWindow]: String(GATEWAY_CONTEXT_WINDOW),
[CLAUDE_K.autoCompactPct]: String(GATEWAY_COMPACT_PCT),
},
});

Expand Down Expand Up @@ -599,6 +611,11 @@ export function configureCodex(creds: Credentials): ConfigureResult[] {
writeToml(sourcePath, {
[CODEX_K.model]: model,
[CODEX_K.modelProvider]: CODEX_K.providerId,
// The gateway model isn't in Codex's catalog, so Codex would otherwise
// assume a 272K fallback window — larger than the real 196608 ceiling.
// Pin the true window and compact at ~85% of it, mirroring Claude Code.
[CODEX_K.modelContextWindow]: GATEWAY_CONTEXT_WINDOW,
[CODEX_K.autoCompactTokenLimit]: GATEWAY_COMPACT_TRIGGER,
[CODEX_K.modelProviders]: {
[CODEX_K.providerId]: {
[CODEX_K.name]: CODEX_K.displayName,
Expand Down Expand Up @@ -660,15 +677,35 @@ export function configureOpenCode(creds: Credentials): ConfigureResult[] {
const allModels =
creds.models && creds.models.length > 0 ? creds.models : [defaultModel];

// A custom-provider model with no `limit` defaults to context 0, which both
// mis-sizes the window and disables OpenCode's auto-compaction entirely.
// Declare the gateway's real window so compaction works; `output` is required
// whenever a `limit` object is present.
const modelsMap = Object.fromEntries(
allModels.map((id) => [id, { [OPENCODE_K.name]: id }]),
allModels.map((id) => [
id,
{
[OPENCODE_K.name]: id,
[OPENCODE_K.limit]: {
[OPENCODE_K.context]: GATEWAY_CONTEXT_WINDOW,
[OPENCODE_K.output]: GATEWAY_MAX_OUTPUT_TOKENS,
},
},
]),
);

writeJson(sourcePath, {
[OPENCODE_K.schema]: OPENCODE_SCHEMA_URL,
// Top-level `model` pins the initial active model OpenCode uses on
// launch. Format is `<provider>/<modelId>` per OpenCode's schema.
[OPENCODE_K.model]: `${OPENCODE_K.providerKey}/${defaultModel}`,
// OpenCode has no percentage trigger; it compacts at `context − reserved`.
// Reserve the headroom that lands the trigger at ~85% of the window, to
// match Claude Code and Codex.
[OPENCODE_K.compaction]: {
[OPENCODE_K.auto]: true,
[OPENCODE_K.reserved]: GATEWAY_COMPACT_RESERVED,
},
[OPENCODE_K.provider]: {
[OPENCODE_K.providerKey]: {
[OPENCODE_K.npm]: OPENCODE_K.npmPkg,
Expand Down
21 changes: 16 additions & 5 deletions src/lib/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,22 @@ export const AI_GATEWAY_OPENAI_URL = `${AI_GATEWAY_URL}/v1`;

export const FALLBACK_MODEL = atob("TWluaU1heC9NaW5pTWF4LU0yLjc=");

// Self-hosted gateway model has a 196608-token window. Telling Claude Code
// to treat that as its effective window and to fire auto-compaction at 85%
// of it (≈167K) keeps compaction well below the hard limit.
export const CLAUDE_AUTO_COMPACT_WINDOW = "196608";
export const CLAUDE_AUTOCOMPACT_PCT = "85";
// The self-hosted gateway model has a 196608-token window. Each agent is told
// to treat that as its effective window and to fire auto-compaction at ~85% of
// it (≈167K), keeping compaction well below the hard limit.
export const GATEWAY_CONTEXT_WINDOW = 196608;
export const GATEWAY_COMPACT_PCT = 85;
// Compaction trigger and reserve, derived from the window and percentage above.
// Codex's `model_auto_compact_token_limit` is an absolute token threshold (≈167K);
// OpenCode has no percentage knob — it compacts at `context − reserved`, so the
// reserve is the headroom that lands the trigger at the same ~85% point.
export const GATEWAY_COMPACT_TRIGGER = Math.round(
GATEWAY_CONTEXT_WINDOW * (GATEWAY_COMPACT_PCT / 100),
);
export const GATEWAY_COMPACT_RESERVED =
GATEWAY_CONTEXT_WINDOW - GATEWAY_COMPACT_TRIGGER;
// Max output tokens advertised to OpenCode (required whenever `limit` is set).
export const GATEWAY_MAX_OUTPUT_TOKENS = 65536;

export const VERSION: string = pkg.version;

Expand Down
22 changes: 22 additions & 0 deletions tests/lib/configure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,16 @@ describe("configureOpenCode", () => {
);
// Top-level `model` pins the active default in <provider>/<modelId> form.
expect(config.model).toBe("aigateway/chosen-model");
// Declares the gateway window so OpenCode sizes context correctly and its
// auto-compaction fires (a model with no `limit` defaults to context 0,
// which disables compaction). `output` is required alongside `context`.
expect(config.provider.aigateway.models["chosen-model"].limit).toEqual({
context: 196608,
output: 65536,
});
// Reserve lands the compaction trigger at ~85% of the window (196608 −
// 29491 ≈ 167K), matching Claude Code and Codex.
expect(config.compaction).toEqual({ auto: true, reserved: 29491 });
});

test("writes every fetched model into the provider's models map", async () => {
Expand All @@ -407,6 +417,7 @@ describe("configureOpenCode", () => {
expect(Object.keys(map).sort()).toEqual(["model-a", "model-b", "model-c"]);
for (const id of ["model-a", "model-b", "model-c"]) {
expect(map[id].name).toBe(id);
expect(map[id].limit).toEqual({ context: 196608, output: 65536 });
}
// Top-level default still points at the chosen one.
expect(config.model).toBe("aigateway/model-a");
Expand Down Expand Up @@ -502,6 +513,8 @@ describe("configureCodex", () => {
) as {
model: string;
model_provider: string;
model_context_window: number;
model_auto_compact_token_limit: number;
model_providers: Record<
string,
{
Expand Down Expand Up @@ -535,6 +548,15 @@ describe("configureCodex", () => {
);
});

test("pins the gateway window and compaction trigger (Codex would otherwise assume a larger fallback window)", async () => {
const { configureCodex } = await import("@/lib/configure.js");
configureCodex({ apiKey: "sk-codex", model: "m" });

const config = readCodexToml();
expect(config.model_context_window).toBe(196608);
expect(config.model_auto_compact_token_limit).toBe(167117); // ≈85% of the window
});

test("does not touch ~/.claude.json (Codex-only install)", async () => {
const { configureCodex } = await import("@/lib/configure.js");
configureCodex({ apiKey: "sk-codex", model: "m" });
Expand Down
17 changes: 17 additions & 0 deletions tests/lib/const.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import {
FALLBACK_MODEL,
GATEWAY_COMPACT_RESERVED,
GATEWAY_COMPACT_TRIGGER,
GATEWAY_CONTEXT_WINDOW,
GATEWAY_MAX_OUTPUT_TOKENS,
SUPABASE_ANON_KEY,
SUPABASE_URL,
} from "@/lib/const.js";
Expand Down Expand Up @@ -90,3 +94,16 @@ describe("FALLBACK_MODEL", () => {
expect(FALLBACK_MODEL).toBe("MiniMax/MiniMax-M2.7");
});
});

describe("gateway compaction constants", () => {
test("window and percentage are the gateway's real values", () => {
expect(GATEWAY_CONTEXT_WINDOW).toBe(196608);
expect(GATEWAY_MAX_OUTPUT_TOKENS).toBe(65536);
});

test("trigger is ~85% of the window and reserve is the remaining headroom", () => {
expect(GATEWAY_COMPACT_TRIGGER).toBe(167117);
expect(GATEWAY_COMPACT_RESERVED).toBe(GATEWAY_CONTEXT_WINDOW - 167117);
expect(GATEWAY_COMPACT_RESERVED).toBe(29491);
});
});
Loading