From 9e7e621436c78a6dd3730cd3d2a27944ba23d82f Mon Sep 17 00:00:00 2001 From: Rouzbeh Date: Fri, 21 Aug 2026 22:41:52 +0000 Subject: [PATCH 1/3] fix(providers): probe dify key validation via chat-messages endpoint (#11002) --- .../config/providers/registry/dify/index.ts | 2 +- src/lib/providers/validation.ts | 3 + .../providers/validation/specialtyInline.ts | 45 +++++++++++++++ tests/unit/account-rotation-lot-c.test.ts | 16 +++--- .../unit/dify-validation-probe-11002.test.ts | 56 +++++++++++++++++++ tests/unit/terminal-status-origin.test.ts | 10 ++-- 6 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 tests/unit/dify-validation-probe-11002.test.ts diff --git a/open-sse/config/providers/registry/dify/index.ts b/open-sse/config/providers/registry/dify/index.ts index de1d5b03cc54..0a5d21308b7c 100644 --- a/open-sse/config/providers/registry/dify/index.ts +++ b/open-sse/config/providers/registry/dify/index.ts @@ -5,7 +5,7 @@ export const difyProvider: RegistryEntry = { alias: "dify", format: "openai", executor: "default", - baseUrl: "https://api.dify.ai/v1/chat/completions", + baseUrl: "https://api.dify.ai/v1", authType: "apikey", authHeader: "bearer", models: [{ id: "auto", name: "Auto" }], diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index 7f8180dee009..96088f0b6ca9 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -122,6 +122,7 @@ import { validateNvidiaProvider, validateZaiProvider, validateXiaomiMimoProvider, + validateDifyProvider, buildGitlawbValidators, } from "./validation/specialtyInline"; // validateCommandCodeProvider + validateClaudeCodeCompatibleProvider have external importers @@ -227,6 +228,8 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi firefly: validateAdobeFireflyProvider, qoder: validateQoderProvider, kiro: validateKiroProvider, + dify: ({ apiKey, providerSpecificData }: any) => + validateDifyProvider({ apiKey, providerSpecificData, isLocal }), freebuff: validateFreebuffProvider, "command-code": validateCommandCodeProvider, huggingface: validateHuggingFaceProvider, diff --git a/src/lib/providers/validation/specialtyInline.ts b/src/lib/providers/validation/specialtyInline.ts index fc9102f5cde6..14a796395d50 100644 --- a/src/lib/providers/validation/specialtyInline.ts +++ b/src/lib/providers/validation/specialtyInline.ts @@ -411,3 +411,48 @@ export function buildGitlawbValidators( ]) ); } + +export async function validateDifyProvider({ apiKey, providerSpecificData, isLocal }: any) { + try { + const configuredBaseUrl = + typeof providerSpecificData?.baseUrl === "string" && providerSpecificData.baseUrl.trim() + ? providerSpecificData.baseUrl.trim() + : "https://api.dify.ai/v1"; + + const root = normalizeBaseUrl(configuredBaseUrl) + .replace(/\/chat\/completions$/, "") + .replace(/\/chat-messages$/, ""); + + const targetUrl = root.endsWith("/v1") ? `${root}/chat-messages` : `${root}/v1/chat-messages`; + + const res = await validationWrite( + targetUrl, + { + method: "POST", + headers: { + ...buildBearerHeaders(apiKey, providerSpecificData), + "Content-Type": "application/json", + }, + body: JSON.stringify({ + inputs: {}, + query: "ping", + response_mode: "blocking", + user: "omniroute-probe", + }), + }, + isLocal + ); + + if (res.ok || res.status === 400) { + return { valid: true, error: null, method: "dify_chat_messages_probe" }; + } + + if (res.status === 401 || res.status === 403) { + return { valid: false, error: "Invalid API key or unauthorized Dify app key" }; + } + + return { valid: false, error: `Dify validation failed (HTTP ${res.status})` }; + } catch (error: any) { + return toValidationErrorResult(error); + } +} diff --git a/tests/unit/account-rotation-lot-c.test.ts b/tests/unit/account-rotation-lot-c.test.ts index d52c0e4bf4dc..0dde306f90a4 100644 --- a/tests/unit/account-rotation-lot-c.test.ts +++ b/tests/unit/account-rotation-lot-c.test.ts @@ -11,7 +11,7 @@ test("markCooldown default is transient — no eviction, only backoff", () => { const a = acct("a"); markCooldown(a); // kind omitted → transient assert.ok(a.cooldownUntil > Date.now()); - assert.equal((a as any).evictedAt, undefined); + assert.equal((a as Record).evictedAt, undefined); // still picked when others are ready const state = { nextAccountIdx: 0 }; const picked = pickAccount([a, acct("b")], state); @@ -25,28 +25,28 @@ test("terminal kind evicts after threshold, pickAccount skips evicted unless all markCooldown(a, "terminal"); markCooldown(a, "terminal"); markCooldown(a, "terminal"); - assert.ok((a as any).evictedAt != null); + assert.ok((a as Record).evictedAt != null); const state = { nextAccountIdx: 0 }; // b is ready, a evicted → b is picked - const picked = pickAccount([a, b], state, (x) => isAccountReady(x) && !(x as any).evictedAt); + const picked = pickAccount([a, b], state, (x) => isAccountReady(x) && !(x as Record).evictedAt); assert.equal(picked.fingerprint, "healthy"); // when all evicted, caller still gets an account rather than hanging (preserves :52-58) - (b as any).evictedAt = Date.now(); - const fallback = pickAccount([a, b], { nextAccountIdx: 0 }, (x) => isAccountReady(x) && !(x as any).evictedAt); + (b as Record).evictedAt = Date.now(); + const fallback = pickAccount([a, b], { nextAccountIdx: 0 }, (x) => isAccountReady(x) && !(x as Record).evictedAt); assert.ok(fallback.fingerprint === "dead" || fallback.fingerprint === "healthy"); }); test("transient does not evict even after many fails — only terminal does", () => { const a = acct("quota-hit"); for (let i = 0; i < 10; i++) markCooldown(a, "transient"); - assert.equal((a as any).evictedAt, undefined); + assert.equal((a as Record).evictedAt, undefined); }); test("markSuccess clears eviction and consecutiveFails", () => { const a = acct("revived"); markCooldown(a, "terminal"); markCooldown(a, "terminal"); markCooldown(a, "terminal"); markSuccess(a); - assert.equal((a as any).evictedAt, null); + assert.equal((a as Record).evictedAt, null); assert.equal(a.consecutiveFails, 0); }); @@ -57,5 +57,5 @@ test("cross-executor alias still works — opencode wrapper forwards kind", asyn assert.ok(mc.length >= 1 && mc.length <= 2); // Prove it accepts terminal without throw const tmp = acct("probe"); - assert.doesNotThrow(() => (mc as any)(tmp, "terminal")); + assert.doesNotThrow(() => (mc as Record)(tmp, "terminal")); }); diff --git a/tests/unit/dify-validation-probe-11002.test.ts b/tests/unit/dify-validation-probe-11002.test.ts new file mode 100644 index 000000000000..4927285ddc48 --- /dev/null +++ b/tests/unit/dify-validation-probe-11002.test.ts @@ -0,0 +1,56 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { validateDifyProvider } from "../../src/lib/providers/validation/specialtyInline.ts"; + +describe("dify provider validation probe (#11002)", () => { + it("probes /v1/chat-messages endpoint and returns valid for HTTP 200/400", async () => { + const originalFetch = globalThis.fetch; + let probedUrl = ""; + let probedMethod = ""; + let probedBody = ""; + + // @ts-ignore + globalThis.fetch = async (url: string | URL | Request, init?: RequestInit) => { + probedUrl = url.toString(); + probedMethod = init?.method || "GET"; + probedBody = (init?.body as string) || ""; + return new Response(JSON.stringify({ code: "invalid_param" }), { status: 400 }); + }; + + try { + const res = await validateDifyProvider({ + apiKey: "test-dify-app-key-12345", + providerSpecificData: { baseUrl: "https://api.dify.ai/v1" }, + isLocal: true, + }); + + assert.equal(probedUrl, "https://api.dify.ai/v1/chat-messages"); + assert.equal(probedMethod, "POST"); + assert.ok(probedBody.includes('"user":"omniroute-probe"')); + assert.equal(res.valid, true); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it("returns invalid for HTTP 401 unauthorized", async () => { + const originalFetch = globalThis.fetch; + // @ts-ignore + globalThis.fetch = async () => { + return new Response(JSON.stringify({ code: "unauthorized" }), { status: 401 }); + }; + + try { + const res = await validateDifyProvider({ + apiKey: "test-dify-app-key-invalid", + providerSpecificData: { baseUrl: "https://api.dify.ai/v1" }, + isLocal: true, + }); + + assert.equal(res.valid, false); + assert.ok(res.error?.includes("Invalid API key")); + } finally { + globalThis.fetch = originalFetch; + } + }); +}); diff --git a/tests/unit/terminal-status-origin.test.ts b/tests/unit/terminal-status-origin.test.ts index f6de47eb6c1e..a4aacf767fc5 100644 --- a/tests/unit/terminal-status-origin.test.ts +++ b/tests/unit/terminal-status-origin.test.ts @@ -11,11 +11,11 @@ const { writeTerminalStatus } = await import("../../src/shared/utils/terminalSta test.after(() => { core.resetDbInstance(); fs.rmSync(DIR, {recursive:true, force:true}); }); -function row(id: string){ return (core.getDbInstance() as any).prepare("SELECT is_active, test_status FROM provider_connections WHERE id=?").get(id); } +function row(id: string){ return (core.getDbInstance() as unknown as Record).prepare("SELECT is_active, test_status FROM provider_connections WHERE id=?").get(id); } test("probe-origin writeTerminalStatus records error but never deactivates", async () => { - const conn = await createProviderConnection({ provider:"openai", authType:"apikey", name:"t11", apiKey:"sk-t11", isActive:true, testStatus:"active" } as any); - const id = String((conn as any).id); + const conn = await createProviderConnection({ provider:"openai", authType:"apikey", name:"t11", apiKey:"sk-t11", isActive:true, testStatus:"active" } as unknown as Record); + const id = String((conn as unknown as Record).id); await runAsProbe(async () => { await writeTerminalStatus(id, { testStatus:"banned", isActive:false, lastError:"probe 403", errorCode:"403", lastErrorType:"FORBIDDEN" }, "probe"); }); @@ -25,8 +25,8 @@ test("probe-origin writeTerminalStatus records error but never deactivates", asy }); test("production writeTerminalStatus deactivates on terminal", async () => { - const conn = await createProviderConnection({ provider:"openai", authType:"apikey", name:"t11b", apiKey:"sk-t11b", isActive:true, testStatus:"active" } as any); - const id = String((conn as any).id); + const conn = await createProviderConnection({ provider:"openai", authType:"apikey", name:"t11b", apiKey:"sk-t11b", isActive:true, testStatus:"active" } as unknown as Record); + const id = String((conn as unknown as Record).id); await writeTerminalStatus(id, { testStatus:"banned", isActive:false, lastError:"real 403", errorCode:"403", lastErrorType:"FORBIDDEN" }, "production"); const r = row(id); assert.equal(r.is_active, 0); From bde751631820afc522505f9f62f97fccfeaf65eb Mon Sep 17 00:00:00 2001 From: Rouzbeh Date: Fri, 21 Aug 2026 22:11:42 +0000 Subject: [PATCH 2/3] fix(build): declare explicit dynamic export in readyz route for Turbopack compatibility --- src/app/readyz/route.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/readyz/route.ts b/src/app/readyz/route.ts index 8111e32b4b21..796db3b2158f 100644 --- a/src/app/readyz/route.ts +++ b/src/app/readyz/route.ts @@ -2,4 +2,5 @@ * Kubernetes-style readiness alias of /healthz. * Same lifecycle phase, same 200/503 bodies. Not a liveness probe. */ -export { dynamic, GET, HEAD } from "../healthz/route"; +export const dynamic = "force-dynamic"; +export { GET, HEAD } from "../healthz/route"; From 2dd9daa9fc9998a33a48170a50a28ce596bfa433 Mon Sep 17 00:00:00 2001 From: Rouzbeh Date: Fri, 21 Aug 2026 23:07:14 +0000 Subject: [PATCH 3/3] fix(ci): ratchet dashboard-typecheck baseline down --- .../quality/dashboard-typecheck-baseline.json | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/config/quality/dashboard-typecheck-baseline.json b/config/quality/dashboard-typecheck-baseline.json index b97762d325e8..b596060a8ef0 100644 --- a/config/quality/dashboard-typecheck-baseline.json +++ b/config/quality/dashboard-typecheck-baseline.json @@ -1,9 +1,6 @@ { - "open-sse/services/payloadRules.ts": { - "TS2677": 1 - }, "src/app/(dashboard)/dashboard/HomePageClient.tsx": { - "TS2339": 16 + "TS2339": 10 }, "src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient.tsx": { "TS2503": 3 @@ -120,10 +117,6 @@ "src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleModelsSection.tsx": { "TS2741": 1 }, - "src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx": { - "TS2345": 3, - "TS2322": 1 - }, "src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsListPanel.tsx": { "TS2322": 2 }, @@ -141,12 +134,6 @@ "src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPlaygroundPanel.tsx": { "TS2503": 1 }, - "src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": { - "TS2322": 1 - }, - "src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelImportHandlers.ts": { - "TS2339": 1 - }, "src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts": { "TS2339": 15 }, @@ -190,9 +177,6 @@ "src/lib/combos/builderDraft.ts": { "TS2741": 1 }, - "src/lib/providers/codexFastTier.ts": { - "TS2367": 1 - }, "src/lib/services/htmlRewriter.ts": { "TS2322": 2, "TS2345": 2 @@ -219,14 +203,7 @@ "src/shared/hooks/useElectron.ts": { "TS2339": 19 }, - "src/shared/providers/webSessionCredentials.ts": { - "TS2353": 1, - "TS2322": 1 - }, "src/shared/schemas/cliCatalog.ts": { "TS2554": 2 - }, - "src/shared/services/opencodeConfig.ts": { - "TS2345": 1 } }