mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-10-19 05:20:05 +00:00
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:
parent
ec710bb44d
commit
094ce0fd54
4 changed files with 134 additions and 1 deletions
1
changelog.d/fixes/15762-openai-like-reasoning-efforts.md
Normal file
1
changelog.d/fixes/15762-openai-like-reasoning-efforts.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
- fix(providers): keep upstream reasoning.supported_efforts when syncing named OpenAI-style providers so /v1/models exposes effort_tiers (#15762)
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
31
tests/unit/openai-like-normalizer-reasoning-15762.test.ts
Normal file
31
tests/unit/openai-like-normalizer-reasoning-15762.test.ts
Normal 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"]);
|
||||
});
|
||||
80
tests/unit/openference-sync-e2e-15762.test.ts
Normal file
80
tests/unit/openference-sync-e2e-15762.test.ts
Normal 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"]);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue