lib/agent-reasoning.test.ts
Rama main · réplica de lectura del repositorio original.
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",
]);
});
});