fix(providers): declare Agnes official thinking effort tiers (#13655)

Declares the accepted thinking-effort tiers per Agnes chat model (2.0/2.5: none/low/medium/high/max; 3.0 adds minimal/xhigh), so the generic declared-tier clamp maps `xhigh`/`off` onto values the upstream accepts instead of forwarding them verbatim and collecting a 400.

Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thanks @HouMinXi!
This commit is contained in:
Bob.Hou
2026-09-16 01:23:53 -04:00
committed by GitHub
parent d589e6b76e
commit d6a0460bd6
3 changed files with 106 additions and 9 deletions

View File

@@ -0,0 +1 @@
- fix(providers): **declare Agnes chat models' live `reasoning_effort` vocabulary so catalog/builder/sanitizer stop inventing aliases the API 400s.** 2.0/2.5 accept `none/low/medium/high/max`; 3.0 also accepts `minimal` and `xhigh`. `off`/`ultra` still clamp off the wire.

View File

@@ -1,5 +1,22 @@
import type { RegistryEntry } from "../../shared.ts";
// Official Agnes chat contract from live /v1/chat/completions probes
// (2026-09-14, apihub.agnes-ai.com). 2.0/2.5 accept none/low/medium/high/max
// and 400 on xhigh/off/ultra/minimal. 3.0 additionally accepts minimal and
// xhigh. HuggingFace's Agnes-3.0-Flash card lists four of these
// (none/low/medium/high); live 3.0 also takes minimal and xhigh, so the
// registry follows the live API rather than the shorter card.
export const AGNES_FLASH_THINKING_EFFORTS = ["none", "low", "medium", "high", "max"] as const;
export const AGNES_30_THINKING_EFFORTS = [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
] as const;
export const agnesProvider: RegistryEntry = {
id: "agnes",
format: "openai",
@@ -15,6 +32,7 @@ export const agnesProvider: RegistryEntry = {
contextLength: 262144,
maxOutputTokens: 65536,
supportsReasoning: true,
supportedThinkingEfforts: [...AGNES_FLASH_THINKING_EFFORTS],
supportsVision: true,
toolCalling: true,
},
@@ -24,18 +42,20 @@ export const agnesProvider: RegistryEntry = {
contextLength: 524288,
maxOutputTokens: 65536,
supportsReasoning: true,
supportedThinkingEfforts: [...AGNES_FLASH_THINKING_EFFORTS],
supportsVision: true,
toolCalling: true,
interleavedField: "reasoning_content",
},
// Wiki (2026-09-10): agnes-3.0-flash is 512k context / 65,536
// output, same window as 2.5-flash. Live /v1/models lists it.
{
// Wiki (2026-09-10) lists agnes-3.0-flash at 512K context / 65,536 max
// output, same window as 2.5 Flash. Live GET /v1/models includes it.
id: "agnes-3.0-flash",
name: "Agnes 3.0 Flash",
contextLength: 524288,
maxOutputTokens: 65536,
supportsReasoning: true,
supportedThinkingEfforts: [...AGNES_30_THINKING_EFFORTS],
supportsVision: true,
toolCalling: true,
interleavedField: "reasoning_content",

View File

@@ -9,13 +9,18 @@ process.env.DATA_DIR = TEST_DATA_DIR;
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
const { VIDEO_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts");
const { REGISTRY: providerRegistry } = await import("../../open-sse/config/providerRegistry.ts");
const { REGISTRY: providerRegistry, getRegistryModelThinkingEfforts } =
await import("../../open-sse/config/providerRegistry.ts");
const { IMAGE_PROVIDERS, getAllImageModels } =
await import("../../open-sse/config/imageRegistry.ts");
const { VIDEO_PROVIDERS, getAllVideoModels } =
await import("../../open-sse/config/videoRegistry.ts");
const { FREE_MODEL_BUDGETS } = await import("../../open-sse/config/freeModelCatalog.ts");
const { DefaultExecutor } = await import("../../open-sse/executors/default.ts");
const { sanitizeReasoningEffortForProvider } =
await import("../../open-sse/executors/base/reasoningEffort.ts");
const { getThinkingCapabilityFields } =
await import("../../src/app/api/v1/models/catalogHelpers.ts");
const { handleImageGeneration } = await import("../../open-sse/handlers/imageGeneration.ts");
const { handleVideoGeneration } = await import("../../open-sse/handlers/videoGeneration.ts");
const { resolveChatCoreTargetFormat } =
@@ -87,6 +92,7 @@ test("agnes ships the current public chat models with the correct capabilities",
assert.equal(flash20.contextLength, 262144);
assert.equal(flash20.maxOutputTokens, 65536);
assert.equal(flash20.supportsReasoning, true);
assert.deepEqual(flash20.supportedThinkingEfforts, ["none", "low", "medium", "high", "max"]);
assert.equal(flash20.supportsVision, true);
assert.equal(flash20.toolCalling, true);
@@ -94,26 +100,97 @@ test("agnes ships the current public chat models with the correct capabilities",
assert.ok(flash25, "agnes-2.5-flash must be defined");
assert.equal(flash25.contextLength, 524288);
assert.equal(flash25.maxOutputTokens, 65536);
assert.equal(flash25.supportsReasoning, true);
assert.deepEqual(flash25.supportedThinkingEfforts, ["none", "low", "medium", "high", "max"]);
const flash30 = entry.models.find((m) => m.id === "agnes-3.0-flash");
assert.ok(flash30, "agnes-3.0-flash must be defined");
assert.equal(flash30.contextLength, 524288);
assert.equal(flash30.maxOutputTokens, 65536);
assert.equal(flash30.supportsReasoning, true);
assert.deepEqual(flash30.supportedThinkingEfforts, [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
]);
assert.equal(flash30.supportsVision, true);
assert.equal(flash30.toolCalling, true);
assert.equal(flash30.interleavedField, "reasoning_content");
});
test("agnes chat models advertise official thinking vocabulary", () => {
for (const id of ["agnes-2.0-flash", "agnes-2.5-flash"]) {
assert.deepEqual(getRegistryModelThinkingEfforts("agnes", id), [
"none",
"low",
"medium",
"high",
"max",
]);
}
assert.deepEqual(getRegistryModelThinkingEfforts("agnes", "agnes-3.0-flash"), [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
]);
});
test("agnes catalog effort_tiers match declared vocabulary, not six-tier fallback", () => {
const efforts = getRegistryModelThinkingEfforts("agnes", "agnes-3.0-flash");
assert.ok(efforts && efforts.length > 0);
assert.deepEqual(
getThinkingCapabilityFields("agnes", "agnes-3.0-flash", true, efforts, !efforts.length),
{
thinking: true,
supportsThinking: true,
effort_tiers: ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
}
);
});
test("agnes sanitizer keeps official tiers and clamps undocumented ones", () => {
const clamp = (model: string, effort: string) =>
(
sanitizeReasoningEffortForProvider({ reasoning_effort: effort }, "agnes", model) as {
reasoning_effort?: string;
}
).reasoning_effort;
assert.equal(clamp("agnes-3.0-flash", "none"), "none");
assert.equal(clamp("agnes-3.0-flash", "minimal"), "minimal");
assert.equal(clamp("agnes-3.0-flash", "low"), "low");
assert.equal(clamp("agnes-3.0-flash", "high"), "high");
assert.equal(clamp("agnes-3.0-flash", "xhigh"), "xhigh");
assert.equal(clamp("agnes-3.0-flash", "max"), "max");
assert.equal(clamp("agnes-3.0-flash", "ultra"), "max");
assert.equal(clamp("agnes-3.0-flash", "off"), "none");
// 2.0/2.5 reject xhigh (HTTP 400); clamp up to the next accepted tier (max).
assert.equal(clamp("agnes-2.0-flash", "xhigh"), "max");
assert.equal(clamp("agnes-2.5-flash", "xhigh"), "max");
assert.equal(clamp("agnes-2.0-flash", "max"), "max");
assert.equal(clamp("agnes-2.0-flash", "off"), "none");
assert.equal(clamp("agnes-2.0-flash", "minimal"), "low");
assert.equal(clamp("agnes-2.5-flash", "minimal"), "low");
assert.equal(clamp("agnes-2.5-flash", "off"), "none");
});
test("agnes registry advertises the live OpenAI-style /models endpoint", () => {
const entry = providerRegistry.agnes;
assert.equal(entry.modelsUrl, AGNES_MODELS_URL);
});
test("agnes is classified for live OpenAI-style /models discovery", async () => {
const { isNamedOpenAIStyleProvider } = await import(
"../../src/app/api/providers/[id]/models/discovery/providerSets.ts"
);
const { isNamedOpenAIStyleProvider } =
await import("../../src/app/api/providers/[id]/models/discovery/providerSets.ts");
assert.equal(isNamedOpenAIStyleProvider("agnes"), true);
});
@@ -125,9 +202,8 @@ test("agnes honors per-connection CN base URL override", () => {
});
test("agnes base-URL field is always-on so CN keys can point at api.agnes-ai.cn", async () => {
const helpers = await import(
"../../src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts"
);
const helpers =
await import("../../src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts");
assert.equal(helpers.isBaseUrlConfigurableProvider("agnes"), true);
assert.equal(helpers.getProviderBaseUrlDefault("agnes"), "https://apihub.agnes-ai.com/v1");
assert.equal(helpers.getProviderBaseUrlPlaceholder("agnes"), AGNES_CN_BASE_URL);