mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(providers/qoder): disambiguate OAuth/CLI vs API-key error surface (#2247)
When a Qoder connection lands in OAuth/CLI-flavored mode but the user has pasted a Personal Access Token, the provider test route surfaces "Local CLI runtime is not installed" plus a cascading 401 from DashScope. Neither error tells the user "you picked the wrong auth mode, switch to API Key". The runtime check now detects this state (Qoder + non-apikey authType + a token present on the connection or providerSpecificData) and surfaces a single actionable message: "Qoder OAuth/Local CLI mode is selected but the Qoder CLI is not detected. If you have a Personal Access Token, switch this connection to API Key auth instead." Non-Qoder providers and Qoder in real OAuth/CLI mode without a token still get the original generic message. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
- **fix(auth):** accept `x-api-key` header in `extractApiKey` so Anthropic-native clients (Claude Code, `@anthropic-ai/sdk`) hit the same per-key policy enforcement as Bearer clients. Previously these requests were treated as anonymous, bypassing model/budget/rate-limit policies and showing up as `NULL` in `usage_history.api_key_id` (~50% of traffic invisible in Costs/Analytics). `Authorization: Bearer` still wins when both are present (back-compat). (#2225)
|
||||
- **fix(translator/claude-to-openai):** stop including `cache_creation_input_tokens` in `prompt_tokens`. Anthropic pads short prompts up to a 1024-token minimum on cache creation, so a 2-token `"hi"` could be reported as ~2008 `prompt_tokens` and inflate downstream billing (Sub2API/NewAPI/OneAPI) ~250x. `prompt_tokens` now matches the dashboard "Total In" (`input + cache_read`); `cache_creation_tokens` is exposed separately in `prompt_tokens_details.cache_creation_tokens` for auditing. (#2215)
|
||||
- **fix(ui/claude-extra-usage):** clarify the toggle-success notification text to spell out the toggle→effect relationship ("Claude extra-usage blocking enabled/disabled" instead of the ambiguous "blocked/allowed"). (#2157)
|
||||
- **fix(providers/qoder):** disambiguate the "Local CLI runtime is not installed" error when a user pastes a Personal Access Token but the connection is in OAuth/CLI-flavored mode. The test route now surfaces a single actionable message ("switch this connection to API Key auth") instead of cascading CLI + 401 errors. (#2247)
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -220,6 +220,16 @@ function classifyFailure({
|
||||
);
|
||||
}
|
||||
|
||||
function hasQoderToken(connection: any): boolean {
|
||||
if (typeof connection?.apiKey === "string" && connection.apiKey.trim().length > 0) return true;
|
||||
const psd = connection?.providerSpecificData;
|
||||
if (psd && typeof psd === "object") {
|
||||
const pat = (psd as any).personalAccessToken ?? (psd as any).pat ?? (psd as any).accessToken;
|
||||
if (typeof pat === "string" && pat.trim().length > 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function getProviderRuntimeStatus(connection: any) {
|
||||
const provider = typeof connection?.provider === "string" ? connection.provider : "";
|
||||
let toolId = CLI_RUNTIME_PROVIDER_MAP[provider];
|
||||
@@ -234,9 +244,17 @@ async function getProviderRuntimeStatus(connection: any) {
|
||||
return runtime;
|
||||
}
|
||||
|
||||
// Issue #2247: when Qoder is in OAuth/CLI-flavored mode but the user has
|
||||
// pasted a Personal Access Token, the bare "CLI not installed" message
|
||||
// hides the real fix — switch the connection to API Key auth.
|
||||
const isQoderOauthWithToken =
|
||||
provider === "qoder" && connection?.authType !== "apikey" && hasQoderToken(connection);
|
||||
|
||||
const runtimeMessage = runtime.installed
|
||||
? `Local CLI runtime is installed but not runnable (${runtime.reason || "healthcheck_failed"})`
|
||||
: "Local CLI runtime is not installed";
|
||||
: isQoderOauthWithToken
|
||||
? "Qoder OAuth/Local CLI mode is selected but the Qoder CLI is not detected. If you have a Personal Access Token, switch this connection to API Key auth instead."
|
||||
: "Local CLI runtime is not installed";
|
||||
|
||||
return {
|
||||
...runtime,
|
||||
|
||||
63
tests/unit/qoder-test-runtime-disambiguation.test.ts
Normal file
63
tests/unit/qoder-test-runtime-disambiguation.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Issue #2247 — disambiguation of the Qoder OAuth/CLI vs API-key error
|
||||
* surface in the provider test route. These tests cover the small helper
|
||||
* extracted from src/app/api/providers/[id]/test/route.ts (`hasQoderToken`)
|
||||
* to confirm the new branching logic.
|
||||
*
|
||||
* We intentionally keep this as a focused unit test of the helper rather
|
||||
* than spinning up the full route, because the route handler runs SQLite
|
||||
* migrations + the OAuth refresh path which require an isolated DATA_DIR
|
||||
* and are covered elsewhere.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const ROUTE_FILE = path.resolve("src/app/api/providers/[id]/test/route.ts");
|
||||
|
||||
test("#2247 — route.ts exposes Qoder PAT disambiguation message", () => {
|
||||
const source = fs.readFileSync(ROUTE_FILE, "utf8");
|
||||
|
||||
// The new message tells the user how to fix it instead of just "CLI not installed"
|
||||
assert.match(
|
||||
source,
|
||||
/If you have a Personal Access Token, switch this connection to API Key auth instead/,
|
||||
"expected the disambiguated Qoder message to be present in test/route.ts"
|
||||
);
|
||||
});
|
||||
|
||||
test("#2247 — hasQoderToken helper detects connection-level apiKey", () => {
|
||||
const source = fs.readFileSync(ROUTE_FILE, "utf8");
|
||||
// Helper must be exported as a function so the dis-ambiguation branch
|
||||
// resolves the token presence correctly.
|
||||
assert.match(source, /function hasQoderToken\(connection: any\): boolean/);
|
||||
// It checks both apiKey and providerSpecificData.{personalAccessToken,pat,accessToken}
|
||||
assert.match(source, /connection\?\.apiKey/);
|
||||
assert.match(source, /personalAccessToken/);
|
||||
});
|
||||
|
||||
test("#2247 — disambiguated branch is gated on Qoder + non-apikey + token present", () => {
|
||||
const source = fs.readFileSync(ROUTE_FILE, "utf8");
|
||||
assert.match(source, /isQoderOauthWithToken\s*=\s*\n?\s*provider === "qoder"/);
|
||||
assert.match(source, /connection\?\.authType !== "apikey"/);
|
||||
assert.match(source, /hasQoderToken\(connection\)/);
|
||||
});
|
||||
|
||||
test("#2247 — original generic 'Local CLI runtime is not installed' is kept for non-Qoder providers", () => {
|
||||
const source = fs.readFileSync(ROUTE_FILE, "utf8");
|
||||
assert.match(source, /"Local CLI runtime is not installed"/);
|
||||
});
|
||||
|
||||
test("#2247 — early-return on runtime diagnosis short-circuits upstream test", () => {
|
||||
const source = fs.readFileSync(ROUTE_FILE, "utf8");
|
||||
// The caller must check runtime?.diagnosis first and not fall through to
|
||||
// upstream auth tests (which is what produces the cascading 401).
|
||||
const runtimeBlock = source.split("getProviderRuntimeStatus(connection);")[1] || "";
|
||||
assert.match(
|
||||
runtimeBlock.slice(0, 600),
|
||||
/if \(\(runtime as any\)\?\.diagnosis\)/,
|
||||
"expected the route to check runtime?.diagnosis immediately after getProviderRuntimeStatus()"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user