import { describe, expect, it } from "vitest"; import { DEFAULT_REASONING_LEVEL, GENERIC_REASONING_LEVELS, isReasoningLevel, nearestReasoningLevel, reasoningLevelsFor, reasoningMaxTokens, reasoningRequestFields, REASONING_LEVELS, toReasoningLevel, } from "./agent-reasoning"; /** * Reasoning level tests (MIN-122). What matters here: nothing is * sent until it is explicitly requested AND the provider has not declared it to the registry — an unknown field returns as 400 and kills the round. */ describe("isReasoningLevel / toReasoningLevel", () => { it("accepte tout le vocabulaire et rien d'autre", () => { for (const level of REASONING_LEVELS) expect(isReasoningLevel(level)).toBe(true); expect(isReasoningLevel("maximum")).toBe(false); // `none` is the OpenRouter word; ours is `off`, and it says a little more // (do not send ANY fields). The translation is done by reading the index. expect(isReasoningLevel("none")).toBe(false); expect(isReasoningLevel("")).toBe(false); expect(isReasoningLevel(null)).toBe(false); expect(isReasoningLevel(2)).toBe(false); }); it("normalizes everything else to the default", () => { expect(toReasoningLevel("high")).toBe("high"); expect(toReasoningLevel("nope")).toBe(DEFAULT_REASONING_LEVEL); expect(toReasoningLevel(undefined)).toBe(DEFAULT_REASONING_LEVEL); }); it("the default is `medium` — the agent reasons a little unless told otherwise", () => { expect(DEFAULT_REASONING_LEVEL).toBe("medium"); }); }); describe("reasoningRequestFields", () => { it("n'envoie RIEN à `off`, quel que soit le provider", () => { expect(reasoningRequestFields("off", "openrouter")).toEqual({}); expect(reasoningRequestFields("off", "openai")).toEqual({}); }); it("n'envoie RIEN sur un niveau inconnu ou absent", () => { expect(reasoningRequestFields(null, "openrouter")).toEqual({}); expect(reasoningRequestFields(undefined, "openrouter")).toEqual({}); expect(reasoningRequestFields("maximum" as never, "openrouter")).toEqual({}); }); it("n'envoie RIEN au provider générique, même à `high`", () => { // Unknown URL base: a strict OpenAI-compatible server rejects the field. expect(reasoningRequestFields("high", "generic")).toEqual({}); }); it("uses the NESTED form on OpenRouter when tracing is requested", () => { expect(reasoningRequestFields("medium", "openrouter")).toEqual({ reasoning: { effort: "medium", exclude: false }, }); }); it("uses the FLAT form on OpenAI and Gemini; Anthropic expects the model", () => { expect(reasoningRequestFields("low", "openai")).toEqual({ reasoning_effort: "low" }); expect(reasoningRequestFields("high", "anthropic")).toEqual({}); expect(reasoningRequestFields("medium", "google")).toEqual({ reasoning_effort: "medium" }); expect(reasoningRequestFields("minimal", "openai")).toEqual({ reasoning_effort: "minimal" }); }); it("passe `xhigh` et `max` tels quels à OpenRouter — c'est SON vocabulaire", () => { expect(reasoningRequestFields("xhigh", "openrouter")).toEqual({ reasoning: { effort: "xhigh", exclude: false }, }); expect(reasoningRequestFields("max", "openrouter")).toEqual({ reasoning: { effort: "max", exclude: false }, }); }); it("but FALLS BACK on compatibility layers that do not know them", () => { // OpenRouter itself falls back on what the model accepts; sent directly, // these two come back to 400 and kill the round. Lose a notch // thinking is better than losing the turn. expect(reasoningRequestFields("xhigh", "openai")).toEqual({ reasoning_effort: "high" }); expect(reasoningRequestFields("max", "anthropic")).toEqual({}); }); it("the model-less function never emits a random Anthropic contract", () => { for (const provider of ["openrouter", "openai", "anthropic", "google", "generic"] as const) { const body = JSON.stringify(reasoningRequestFields("high", provider)); expect(body).not.toContain("budget_tokens"); expect(body).not.toContain("thinkingConfig"); expect(body).not.toContain("thinking"); } }); }); describe("reasoningMaxTokens", () => { it("leaves the cap intact at `off` or for an invalid level", () => { expect(reasoningMaxTokens(8192, "off")).toBe(8192); expect(reasoningMaxTokens(8192, null)).toBe(8192); }); it("raises the cap higher and higher with the level", () => { const off = reasoningMaxTokens(8192, "off")!; const low = reasoningMaxTokens(8192, "low")!; const medium = reasoningMaxTokens(8192, "medium")!; const high = reasoningMaxTokens(8192, "high")!; expect(off).toBeLessThan(low); expect(low).toBeLessThan(medium); expect(medium).toBeLessThan(high); }); it("reste absent quand le provider n'envoie pas de max_tokens", () => { expect(reasoningMaxTokens(undefined, "high")).toBeUndefined(); }); }); describe("reasoningLevelsFor — what the selector offers", () => { it("without metadata, the four historical levels", () => { // A non-index model or a catalog still loading: we know nothing, we // promise nothing more than what worked before. expect(reasoningLevelsFor(undefined)).toEqual(GENERIC_REASONING_LEVELS); }); it("no level at all when no reasoning control reaches the model", () => { // A model without reasoning, or an endpoint that drops every field. expect(reasoningLevelsFor(null)).toEqual([]); }); it("the levels PUBLISHED by the model, preceded by « no reasoning »", () => { // `openai/gpt-5.1-codex-max` en vrai : xhigh|high|medium|low, non obligatoire. expect( reasoningLevelsFor({ efforts: ["low", "medium", "high", "xhigh"], mandatory: false }), ).toEqual(["off", "low", "medium", "high", "xhigh"]); }); it("a model that ALWAYS reasons has no « no reasoning » level", () => { // `google/gemini-3.6-flash` en vrai : mandatory, minimal|low|medium|high. Lui // sending `none` breaks the call — so the option should not exist on the screen. expect( reasoningLevelsFor({ efforts: ["minimal", "low", "medium", "high"], mandatory: true }), ).toEqual(["minimal", "low", "medium", "high"]); }); it("a model that reasons without publishing an enumeration falls back to the generic levels", () => { // Les Claude: `{ mandatory: false }`, and nothing else. expect(reasoningLevelsFor({ efforts: [], mandatory: false })).toEqual( GENERIC_REASONING_LEVELS, ); }); }); describe("nearestReasoningLevel", () => { it("keeps the level when the model accepts it", () => { expect(nearestReasoningLevel("high", ["low", "medium", "high"])).toBe("high"); }); it("descend d'abord — on ne dépense jamais plus que demandé", () => { expect(nearestReasoningLevel("max", ["low", "medium", "high"])).toBe("high"); expect(nearestReasoningLevel("medium", ["off", "low"])).toBe("low"); }); it("but moves back up when the model has nothing cheaper", () => { // A model that only accepts `high` must receive `high`, even on a default // personal to `low`: the selector cannot display a level that it does not list. expect(nearestReasoningLevel("low", ["high"])).toBe("high"); }); it("laisse passer quand on ne sait rien de ce qu'il accepte", () => { expect(nearestReasoningLevel("xhigh", [])).toBe("xhigh"); }); it("the entire vocabulary is ordered from cheapest to most expensive", () => { // The rank of `REASONING_LEVELS` IS the scale that `nearestReasoningLevel` // goes down: putting them out of order would collapse `max` to `minimal`. expect(REASONING_LEVELS).toEqual([ "off", "minimal", "low", "medium", "high", "xhigh", "max", ]); }); });