fix(providers): keep upstream reasoning efforts in OpenAI-like model discovery (#15762) (#15815)

Batch-merged with the owner's 2026-10-10 hold/backlog PR wave (/merge-batch). Validated in one combined tree of 43 boarded PRs on top of release/v3.8.52 (048d9e2f): typecheck:core, check:open-sse-typecheck, check-deps, check-test-discovery, changelog-integrity, complexity + cognitive ratchets, check:docs-all, i18n UI coverage, ESLint on the 142 changed source files - green; 58 changed node:test files 584/586 (2 timing flakes in video-retention-http-12150 that pass in isolation) and 3 vitest files 116/116. File-size overruns in that tree belong only to PRs held out of this wave (#16110, #16112, #16140, #15790). Warning: base-red inherited: #15306.
This commit is contained in:
Diego Rodrigues de Sa e Souza 2026-10-10 17:26:02 -03:00 • committed by GitHub
parent ec710bb44d
commit 094ce0fd54
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 134 additions and 1 deletions

View file

@ -0,0 +1 @@
- fix(providers): keep upstream reasoning.supported_efforts when syncing named OpenAI-style providers so /v1/models exposes effort_tiers (#15762)

View file

@ -240,6 +240,27 @@ export function normalizeDataRobotCatalogResponse(
.filter((value): value is { id: string; name: string } => Boolean(value));
}
// Upstream fields that declare supported reasoning efforts (#15762). Only these are carried
// through so the sync extractor sees them; spreading the whole record would alter other flows.
const OPENAI_LIKE_EFFORT_DECLARATION_KEYS = [
"reasoning",
"supported_reasoning_levels",
"thinking",
"metadata",
"capabilities",
"supportedThinkingEfforts",
"reasoning_options",
"vendors",
] as const;
function pickEffortDeclarations(item: Record<string, unknown>): Record<string, unknown> {
const picked: Record<string, unknown> = {};
for (const key of OPENAI_LIKE_EFFORT_DECLARATION_KEYS) {
if (item[key] !== undefined && item[key] !== null) picked[key] = item[key];
}
return picked;
}
export function normalizeOpenAiLikeModelsResponse(
data: unknown,
fallbackOwner: string
@ -266,7 +287,7 @@ export function normalizeOpenAiLikeModelsResponse(
id;
const ownedBy =
toNonEmptyString(item.owned_by) || toNonEmptyString(item.provider) || fallbackOwner;
return { id, name, owned_by: ownedBy };
return { ...pickEffortDeclarations(item), id, name, owned_by: ownedBy };
})
.filter((value): value is { id: string; name: string; owned_by: string } => Boolean(value));
}

View file

@ -0,0 +1,31 @@
import test from "node:test";
import assert from "node:assert/strict";
const { normalizeOpenAiLikeModelsResponse } =
await import("../../src/app/api/providers/[id]/models/discovery/normalizers.ts");
test("normalizeOpenAiLikeModelsResponse keeps reasoning effort declarations (#15762)", () => {
const out = normalizeOpenAiLikeModelsResponse(
{
data: [
{
id: "GLM-5.3",
reasoning: { supported: true, supported_efforts: ["max", "high", "low"] },
context_length: 1234,
pricing: { prompt: "1" },
},
{ id: "plain" },
],
},
"owner"
) as Array<Record<string, unknown>>;
assert.deepEqual((out[0].reasoning as { supported_efforts: string[] }).supported_efforts, [
"max",
"high",
"low",
]);
assert.equal(out[0].owned_by, "owner");
assert.equal("context_length" in out[0], false);
assert.equal("pricing" in out[0], false);
assert.deepEqual(Object.keys(out[1]).sort(), ["id", "name", "owned_by"]);
});

View file

@ -0,0 +1,80 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-15762e2e-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.CATALOG_BUILD_TIMEOUT_MS = "120000"; // cold catalog build can exceed 8s on loaded CI hosts
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "catalog-test-secret-15762";
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const syncModelsRoute = await import("../../src/app/api/providers/[id]/sync-models/route.ts");
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
const { buildModelSyncInternalHeaders } =
await import("../../src/shared/services/modelSyncScheduler.ts");
const originalFetch = globalThis.fetch;
test.after(() => {
globalThis.fetch = originalFetch;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
const UPSTREAM = {
object: "list",
data: [
{
id: "GLM-5.3",
object: "model",
capabilities: { tool_calling: true, vision: false, attachment: false },
reasoning: { supported: true, control: "effort", supported_efforts: ["max", "high", "low"] },
},
],
};
for (const [provider, authType] of [
["openference", "oauth"],
["openference-api", "apikey"],
] as const) {
test(`${provider}: sync-models then /v1/models exposes effort_tiers`, async () => {
v1ModelsCatalog.__resetCatalogBuilderRunsForTest();
const conn = await providersDb.createProviderConnection({
provider,
authType,
name: `${provider}-e2e`,
...(authType === "oauth" ? { accessToken: "jwt" } : { apiKey: "sk-x" }),
isActive: true,
testStatus: "active",
providerSpecificData: { autoFetchModels: true },
});
const calls: string[] = [];
globalThis.fetch = async (url: unknown) => {
const u = String(url);
calls.push(u);
if (u.includes("api.openference.com/v1/models")) return Response.json(UPSTREAM);
if (u.includes("__readiness_probe__")) return new Response("", { status: 404 });
throw new Error("force in-process fallback: " + u);
};
const res = await syncModelsRoute.POST(
new Request(`http://127.0.0.1/api/providers/${conn.id}/sync-models`, {
method: "POST",
headers: buildModelSyncInternalHeaders(),
}),
{ params: Promise.resolve({ id: conn.id }) }
);
assert.equal(res.status, 200);
globalThis.fetch = originalFetch;
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as {
data: Array<{ id: string; capabilities?: { effort_tiers?: string[] } }>;
};
const glm = body.data.filter((m) => m.id.endsWith("/GLM-5.3"));
assert.ok(glm.length > 0);
assert.deepEqual(glm[0].capabilities?.effort_tiers, ["max", "high", "low"]);
});
}