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", () => {