From 2b555711640e79f3dccb2efc89fa7659e7d2cc95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9E=AC=ED=98=84?= Date: Mon, 21 Sep 2026 17:25:50 +0900 Subject: [PATCH] Expose reasoning controls in GJC model exports GJC accepts per-model effort metadata, but the client exporter discarded the proxy catalog ladder and hid its thinking controls. Carry declared ladders into the generated models.yml and supply the standard native Codex ladder when the native catalog does not repeat it. Constraint: GJC model entries are strictly validated, so only its documented reasoning, thinking, and compat fields are emitted Rejected: Use the proxy-wide effort cap for each model | concurrent sessions could overwrite one another Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep ultra out of exported GJC levels; OpenCodex folds it to max on the wire Tested: bun test tests/config/client-config-export.test.ts tests/config/client-config-export-new-clients.test.ts; bun run typecheck; bun run privacy:scan Not-tested: Full repository suite --- src/clients/config-export.ts | 33 +++++++++++++ .../client-config-export-new-clients.test.ts | 48 ++++++++++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/clients/config-export.ts b/src/clients/config-export.ts index 90fe5030563..7c5bfaef8d4 100644 --- a/src/clients/config-export.ts +++ b/src/clients/config-export.ts @@ -898,6 +898,17 @@ export interface GajaeModelEntry { input: string[]; contextWindow?: number; maxTokens?: number; + /** Advertised only when the catalog declares at least one wire-selectable effort. */ + reasoning?: true; + /** GJC's per-model reasoning-effort capability declaration. */ + thinking?: { + mode: "effort"; + minLevel: string; + maxLevel: string; + levels: string[]; + }; + /** Tells GJC to pass the selected level as OpenAI-compatible reasoning_effort. */ + compat?: { supportsReasoningEffort: true }; } /** Gajae validates strictly: an unknown field fails the whole config. */ @@ -1086,6 +1097,28 @@ function buildGajaeClientConfig(ctx: ExportContext): GajaeGeneratedConfig { entry.contextWindow = context; entry.maxTokens = outputBudgetFor(context); } + // GJC accepts the OpenAI-compatible effort ladder in model metadata. `none` means + // no parameter and `ultra` is an OCX orchestration level that folds to `max` on the + // wire, so neither can be offered as a GJC model-level effort. + const declaredEfforts = model.reasoningEfforts + // Native Codex rows do not repeat their built-in ladder in the catalog. They still + // accept the standard effort field, so omitting this fallback hides GJC's thinking + // control for the models most likely to need it. + ?? (model.native && model.provider === "openai" + ? ["low", "medium", "high", "xhigh", "max"] + : []); + const efforts = canonicalizeReasoningEfforts(declaredEfforts) + .filter(effort => effort !== "none" && effort !== "ultra"); + if (efforts.length > 0) { + entry.reasoning = true; + entry.thinking = { + mode: "effort", + minLevel: efforts[0]!, + maxLevel: efforts.at(-1)!, + levels: efforts, + }; + entry.compat = { supportsReasoningEffort: true }; + } models.push(entry); } return { diff --git a/tests/config/client-config-export-new-clients.test.ts b/tests/config/client-config-export-new-clients.test.ts index 03000ae38b3..2ede34d7cc6 100644 --- a/tests/config/client-config-export-new-clients.test.ts +++ b/tests/config/client-config-export-new-clients.test.ts @@ -297,7 +297,7 @@ describe("gajae", () => { expect(block.apiKey).toBe(LOOPBACK_API_KEY_PLACEHOLDER); expect(block).not.toHaveProperty("apiKeyEnv"); expect(Object.keys(block).sort()).toEqual(["api", "apiKey", "baseUrl", "models"]); - const allowed = new Set(["id", "name", "input", "contextWindow", "maxTokens"]); + const allowed = new Set(["id", "name", "input", "contextWindow", "maxTokens", "reasoning", "thinking", "compat"]); for (const model of block.models) { for (const key of Object.keys(model)) expect(allowed.has(key)).toBe(true); } @@ -306,6 +306,52 @@ describe("gajae", () => { test("the destination is the documented models file", () => { expect(gajaeConfigPath({}, "/home/u")).toBe(join("/home/u", ".gjc", "agent", "models.yml")); }); + + test("exports a declared effort ladder as GJC reasoning metadata", () => { + const doc = buildClientConfig("gajae", { + ...ctx(), + models: [{ + namespaced: "deepseek/deepseek-v4.1-flash", + provider: "deepseek", + id: "deepseek-v4.1-flash", + inputModalities: ["text"], + reasoningEfforts: ["max", "low", "medium", "high", "xhigh", "none", "turbo"], + }], + }) as GajaeGeneratedConfig; + + expect(doc.providers[OPENCODE_PROVIDER_ID]!.models).toEqual([{ + id: "deepseek/deepseek-v4.1-flash", + name: "deepseek-v4.1-flash (deepseek)", + input: ["text"], + reasoning: true, + thinking: { + mode: "effort", + minLevel: "low", + maxLevel: "max", + levels: ["low", "medium", "high", "xhigh", "max"], + }, + compat: { supportsReasoningEffort: true }, + }]); + }); + + test("exports the native Codex effort ladder even when the catalog omits it", () => { + const doc = buildClientConfig("gajae", { + ...ctx(), + models: [{ + namespaced: "gpt-5.6-sol", + provider: "openai", + id: "gpt-5.6-sol", + native: true, + inputModalities: ["text", "image"], + }], + }) as GajaeGeneratedConfig; + + expect(doc.providers[OPENCODE_PROVIDER_ID]!.models[0]).toMatchObject({ + reasoning: true, + thinking: { mode: "effort", levels: ["low", "medium", "high", "xhigh", "max"] }, + compat: { supportsReasoningEffort: true }, + }); + }); }); describe("contributions name every fragment we own", () => {