mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 00:52:18 +03:00
Two defects exposed by #11226 (the 401 "User not found." itself is upstream OpenRouter's response to a bad key — not an OmniRoute bug): 1. OpenRouter key validation was vacuous. Both the dashboard Check (via validateProviderApiKey -> validateOpenAILikeProvider) and 'omniroute providers test' (bin/cli/provider-test.mjs) probed /api/v1/models, which is PUBLIC and answers 200 to any key — so invalid keys were saved/marked valid and only failed on real chat traffic. OpenRouter's authenticated key-info endpoint (GET /api/v1/auth/key, 200 = valid / 401 = invalid) is now the probe: registered as testKeyModelsUrl on the openrouter registry entry (same mechanism as perplexity) and as keyCheckPath in the CLI test configs. No other provider's probe changes; error results keep using canned strings, never the raw upstream body (Hard Rule #12). 2. 'omniroute auth export' crashed with "cmd.optsWithGlobals is not a function". .command("auth export") does not register a two-word command: commander parses the bare word 'export' as a required positional argument, so the action received (exportArgValue, options, command) while expecting (options, command). Registered 'export' as a proper nested subcommand of 'auth' — the documented CLI surface 'omniroute auth export [--force] [--id] [--format] [--out]' is unchanged, and unknown positionals (e.g. 'omniroute auth bogus') are now rejected instead of silently running the export. TDD: tests/unit/openrouter-key-validation-auth-endpoint.test.ts (stub mimics the real OpenRouter: /models public-200, /auth/key 401 'User not found.') and tests/unit/cli-auth-export-wiring.test.ts (real commander wiring via createProgram) were RED before the fix and are GREEN after. Co-authored-by: Xiangzhe <bakryun0718@proton.me>
153 lines
5.6 KiB
TypeScript
153 lines
5.6 KiB
TypeScript
// #11226 — OpenRouter key validation was vacuous: the probe targeted the PUBLIC
|
|
// /api/v1/models endpoint, which answers 200 to any key (or no key at all), so a
|
|
// bad key was saved as "valid" and only failed later on real chat traffic with the
|
|
// upstream 401 "User not found.". The authenticated key-info endpoint
|
|
// (/api/v1/auth/key) is the correct probe: 200 = valid, 401 = invalid.
|
|
//
|
|
// The fetch stubs below mimic the REAL OpenRouter behavior verified live:
|
|
// GET /api/v1/models → 200 without any auth (public catalog)
|
|
// GET /api/v1/auth/key → 401 {"error":{"message":"User not found.","code":401}} for a bad key
|
|
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts");
|
|
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
|
const { testProviderApiKey } = await import("../../bin/cli/provider-test.mjs");
|
|
|
|
const AUTH_KEY_URL = "https://openrouter.ai/api/v1/auth/key";
|
|
const PUBLIC_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
|
|
const BAD_KEY = "sk-or-v1-definitely-invalid-key";
|
|
const GOOD_KEY = "sk-or-v1-valid-key";
|
|
|
|
interface RecordedCall {
|
|
url: string;
|
|
authorization: string | null;
|
|
}
|
|
|
|
/**
|
|
* Stub fetch with the real OpenRouter behavior: /models is public (always 200),
|
|
* /auth/key requires a valid bearer (401 "User not found." otherwise).
|
|
*/
|
|
function stubRealOpenRouter() {
|
|
const calls: RecordedCall[] = [];
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
const headers = new Headers(
|
|
init?.headers ?? (input instanceof Request ? input.headers : undefined)
|
|
);
|
|
calls.push({ url, authorization: headers.get("authorization") });
|
|
|
|
if (url.startsWith(AUTH_KEY_URL)) {
|
|
const bearer = headers.get("authorization") || "";
|
|
if (bearer === `Bearer ${GOOD_KEY}`) {
|
|
return new Response(JSON.stringify({ data: { label: "ok", is_free_tier: false } }), {
|
|
status: 200,
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({ error: { message: "User not found.", code: 401 } }), {
|
|
status: 401,
|
|
});
|
|
}
|
|
if (url.includes("/models")) {
|
|
// Public catalog — answers 200 regardless of the Authorization header.
|
|
return new Response(JSON.stringify({ data: [] }), { status: 200 });
|
|
}
|
|
return new Response("{}", { status: 404 });
|
|
}) as typeof fetch;
|
|
return {
|
|
calls,
|
|
restore: () => {
|
|
globalThis.fetch = originalFetch;
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("openrouter registry — authenticated key-validation endpoint (#11226)", () => {
|
|
it("declares the authenticated /auth/key probe as its key-test endpoint", () => {
|
|
const entry = getRegistryEntry("openrouter");
|
|
assert.ok(entry, "openrouter must be registered in the execution registry");
|
|
assert.equal(entry.testKeyModelsUrl, AUTH_KEY_URL);
|
|
});
|
|
|
|
it("marks a bad key INVALID even though the public /models endpoint answers 200", async () => {
|
|
const stub = stubRealOpenRouter();
|
|
try {
|
|
const result = await validateProviderApiKey({ provider: "openrouter", apiKey: BAD_KEY });
|
|
assert.equal(result.valid, false, "bad key must not validate against the public catalog");
|
|
assert.equal(result.error, "Invalid API key");
|
|
assert.deepEqual(
|
|
stub.calls.map((c) => c.url),
|
|
[AUTH_KEY_URL],
|
|
"must probe the authenticated key endpoint, not the public /models"
|
|
);
|
|
assert.equal(stub.calls[0].authorization, `Bearer ${BAD_KEY}`);
|
|
} finally {
|
|
stub.restore();
|
|
}
|
|
});
|
|
|
|
it("marks a good key VALID via /auth/key and never falls back to the chat probe", async () => {
|
|
const stub = stubRealOpenRouter();
|
|
try {
|
|
const result = await validateProviderApiKey({ provider: "openrouter", apiKey: GOOD_KEY });
|
|
assert.equal(result.valid, true);
|
|
assert.equal(result.error, null);
|
|
assert.deepEqual(
|
|
stub.calls.map((c) => c.url),
|
|
[AUTH_KEY_URL]
|
|
);
|
|
} finally {
|
|
stub.restore();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("omniroute providers test — openrouter probe (#11226)", () => {
|
|
it("marks a bad key INVALID even though the public /models endpoint answers 200", async () => {
|
|
const stub = stubRealOpenRouter();
|
|
try {
|
|
const result = await testProviderApiKey({ provider: "openrouter", apiKey: BAD_KEY });
|
|
assert.equal(result.valid, false, "CLI test must not trust the public /models endpoint");
|
|
assert.equal(result.error, "Invalid API key");
|
|
assert.deepEqual(
|
|
stub.calls.map((c) => c.url),
|
|
[AUTH_KEY_URL]
|
|
);
|
|
} finally {
|
|
stub.restore();
|
|
}
|
|
});
|
|
|
|
it("marks a good key VALID via /auth/key", async () => {
|
|
const stub = stubRealOpenRouter();
|
|
try {
|
|
const result = await testProviderApiKey({ provider: "openrouter", apiKey: GOOD_KEY });
|
|
assert.equal(result.valid, true);
|
|
assert.equal(result.error, null);
|
|
assert.deepEqual(
|
|
stub.calls.map((c) => c.url),
|
|
[AUTH_KEY_URL]
|
|
);
|
|
} finally {
|
|
stub.restore();
|
|
}
|
|
});
|
|
|
|
it("does not change the probe for other OpenAI-like providers (openai still uses /models)", async () => {
|
|
const stub = stubRealOpenRouter();
|
|
try {
|
|
const result = await testProviderApiKey({ provider: "openai", apiKey: GOOD_KEY });
|
|
assert.equal(result.valid, true);
|
|
assert.deepEqual(
|
|
stub.calls.map((c) => c.url),
|
|
["https://api.openai.com/v1/models"]
|
|
);
|
|
assert.ok(!stub.calls.some((c) => c.url === PUBLIC_MODELS_URL));
|
|
} finally {
|
|
stub.restore();
|
|
}
|
|
});
|
|
});
|