feat(settings): the OpenRouter key gets its live check — "Check key" beside the field #273
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/openrouter-check-key"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The settings story's last open box (§1): a Check key button under the OpenRouter API key that asks the real API whether the stored key is accepted and shows the verdict inline. Mirrors the NovelAI Check token flow end to end, so the two credential sections now behave the same.
What the user sees
User not found.The path, layer by layer
IOpenRouterGateway.ValidateKeyAsync(UseCases port) +CheckOpenRouterKeyuse case — reads the stored secret server-side; the UI asks for a verdict, never the value. Registered inAddUseCases.OpenRouterGateway.ValidateKeyAsync(Infrastructure) — a fresh namedHttpClientwithAuthorization: Bearer <key>,GET /keythrough the vendoredOpenRouterClient, retries off, 30 s ceiling; failures and exceptions becomeErrwith the API's message.OpenRouterSettingsState—Checking/Verdict(the sharedTokenVerdict),CheckOpenRouterKeyRequested/OpenRouterKeyChecked, the save reducer voids the verdict,OnFailedalso resetsChecking, and the effect catches like its siblings.OpenRouterSettingsSection.razor+ scoped.or__checkrow.No new user-authored content, so the dev seed and the assistant tools are unchanged; no ADR — no decision was made that the NovelAI sibling had not already made.
Verified
dotnet build -c Release && dotnet test -c Release --no-build: 340 / 874 / 790 — 2004 tests, +8 (four section tests inOpenRouterSettingsSectionTests, three gateway tests inOpenRouterGatewayTests, one no-key test inOpenRouterSettingsTests; the fourIOpenRouterGatewayfakes learned the member).GET https://openrouter.ai/api/v1/keywithBearer <key>, and the public catalog fetch still goes out without the header. Mutation check: schemeBearer→Token, build succeeded, exactlyA_key_the_api_accepts_is_valid_and_travels_as_a_bearer_token_to_the_key_endpointwent red (6/7 green), restored, green.openrouter.ai/api/v1/keywith a junk key:{"error":{"message":"User not found.","code":401}}.Not exercised
dataenvelope test and is the same code path as the 401 minus the branch.🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagura.BlazorAdapter - 89%
Kagura.Domain - 94.8%
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
Kagura.Infrastructure - 97.7%
n
n
on
ng
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__ArtifactTimestampRegex_2
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__ProjectRoute_0
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__TabQuery_1
Kagura.Kernel - 90%
Kagura.Server - 81.5%
Kagura.UseCases - 95.5%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! The settings story's last little box, ticking itself closed — and a perfect mirror of the NovelAI sibling while doing it. I read this diff twice just to watch the symmetry, fufu~ ♡ Port, use case, gateway, state, effect, razor, CSS — every layer lands where its sibling landed. That's not an accident; that's discipline.
Verdict: ✅ Looks good to me~
✅ What I liked~
CheckOpenRouterKeyisCheckNovelAiTokenwith the names changed; the gateway reuses the sibling'sValidationTimeout = 30sfail-fast doctrine and build-per-call approach (correct — the library's DI path bakes a startup key into cached options, and the check must run with whatever key is stored right now); the state machine voids the verdict on save exactly likeOnSaveTokenRequested;.or__checkis.nai__checkbyte-for-byte. TheTokenVerdictdoc comment generalized to cover both sections — tidy~HasKeyand a verdict.A_valid_check_says_so_inline_and_validates_the_stored_keyasserts the stored secret is what got validated (ValidatedKeys == ["sk-or-good"]), andThe_key_check_does_not_lend_its_key_to_the_public_catalog_fetchpins that the Authorization header rides only the check's freshHttpClientinstance, never the public catalog request. I traced the vendored client to confirm the premise:CreateClienthands out a fresh instance, and nothing else mutatesDefaultRequestHeaders.RetryOptions.Disabledwas the right call, and correctly justified — a 401 is the answer, not something to re-ask three times with backoff. I checked the vendoredRetryOptions.cs:MaxAttempts = 1, empty retryable set, no network retry, noRetry-After. Genuinely off.GET https://openrouter.ai/api/v1/key,Bearer sk-or-secret); the 401 body is the live-probed one; the crash test asserts the button un-sticks. And I mutation-probed the reducer myself: removingVerdict = nullfromOnSaveKeyRequested→Replacing_the_key_voids_the_previous_verdictgoes red. A test that bites~ ♪ErrorParserandParseResponseAsyncin the vendored client: a 401 envelope surfaces"User not found."asFailure.Error, and the parser can never emit an empty message — so the gateway'sIsNullOrWhiteSpacefallback is belt-and-suspenders, exactly where it should be.💡 Little ideas (non-blocking)~
private void Check() => Dispatcher.Dispatch(...)as a named method; this one inlines the lambda in the markup. Purely stylistic — sibling symmetry would be sweet, but the behavior is identical.catch (Exception)arm (network fault → friendly message) has no direct test. The vendored client already converts transport faults toFailureresults internally, so the arm guards against client-constructor or future throws — the sibling's equivalent arm is equally untested, so parity holds. A throwingStubHandlerwould pin it, someday, if you're bored~Automated review by Jibril · 2026-09-05
CI/CD: absent for head SHA
d328f3b· Local checks: build 0w/0e (.NET 10) · 2004/2004 tests pass (340 UseCases + 874 BlazorAdapter + 790 Integration — matches PR body exactly) · mutation probe RED→restored→green · clone left pristine atd328f3b