Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 1 addition & 24 deletions config/quality/dashboard-typecheck-baseline.json
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
},
Expand All @@ -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
},
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}
2 changes: 1 addition & 1 deletion open-sse/config/providers/registry/dify/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }],
Expand Down
3 changes: 2 additions & 1 deletion src/app/readyz/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
3 changes: 3 additions & 0 deletions src/lib/providers/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ import {
validateNvidiaProvider,
validateZaiProvider,
validateXiaomiMimoProvider,
validateDifyProvider,
buildGitlawbValidators,
} from "./validation/specialtyInline";
// validateCommandCodeProvider + validateClaudeCodeCompatibleProvider have external importers
Expand Down Expand Up @@ -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,
Expand Down
45 changes: 45 additions & 0 deletions src/lib/providers/validation/specialtyInline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
16 changes: 8 additions & 8 deletions tests/unit/account-rotation-lot-c.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>).evictedAt, undefined);
// still picked when others are ready
const state = { nextAccountIdx: 0 };
const picked = pickAccount([a, acct("b")], state);
Expand All @@ -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<string, unknown>).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<string, unknown>).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<string, unknown>).evictedAt = Date.now();
const fallback = pickAccount([a, b], { nextAccountIdx: 0 }, (x) => isAccountReady(x) && !(x as Record<string, unknown>).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<string, unknown>).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<string, unknown>).evictedAt, null);
assert.equal(a.consecutiveFails, 0);
});

Expand All @@ -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<string, unknown>)(tmp, "terminal"));
});
56 changes: 56 additions & 0 deletions tests/unit/dify-validation-probe-11002.test.ts
Original file line number Diff line number Diff line change
@@ -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;
}
});
});
10 changes: 5 additions & 5 deletions tests/unit/terminal-status-origin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>).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<string, unknown>);
const id = String((conn as unknown as Record<string, unknown>).id);
await runAsProbe(async () => {
await writeTerminalStatus(id, { testStatus:"banned", isActive:false, lastError:"probe 403", errorCode:"403", lastErrorType:"FORBIDDEN" }, "probe");
});
Expand All @@ -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<string, unknown>);
const id = String((conn as unknown as Record<string, unknown>).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);
Expand Down
Loading