feat(catalog): map unmapped free tiers, add navy + aihorde, surface keyless providers - #7840
Conversation
…eyless Seven providers whose free tier was documented upstream but never reached our catalog. Five of them we could already route — only the quota was missing. Providers already routable, quota now mapped: - requesty (200 req/day), ovhcloud (2 req/min per IP, anonymous), agnes (permanently free), glm (GLM-4.7/4.5-Flash are Free on the official pricing table). All registered as recurring-uncapped: their free tier is capped in REQUESTS, not tokens, so inventing a token figure would inflate the headline. The "~30M/month" that circulates for GLM belongs to BigModel.cn (a separate Chinese offering) and is deliberately not recorded. New providers: - navy: one shared 150K tokens/day pool (~4.5M/month) drained by a per-model token_multiplier. Registered as a SINGLE pooled row — summing its ~149 free models would overcount ~149x. - aihorde: crowdsourced volunteer GPUs, keyless via the documented anonymous key. No tool calling and a 120s timeout, because requests queue for minutes. Also: - kilo-gateway reconciled against its live /models list (7 -> 13 models) and flagged with the new trainsOnPrompts field: every free Kilo model reports mayTrainOnYourPrompts: true, so the privacy cost now sits next to the quota. - Free-tier page gains search, provider/keyless filters, per-row type badges, a "no API key required" section and a curation-date freshness indicator. - catalogUpdatedAt comes from an explicit FREE_CATALOG_CURATED_AT constant rather than the data file's mtime: a standalone build rewrites timestamps on deploy, which would advertise a months-old catalog as updated today. Net effect on the headline: 462 -> 484 models but 1.371B -> 1.376B tokens, because only navy publishes a token quota. That is the point — coverage grows without the number lying.
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
"Works without a credential" lived in three registries that disagreed, and only three providers were classified the same way in all of them: - NOAUTH_PROVIDERS.noAuth -> whether the connect form hides the field - RegistryEntry.authType / anonymousApiKey -> what the executor really sends - FreeModelBudget.freeType === "keyless" -> how the catalog labels it getCredentialRequirement() now derives the answer from the two sources that describe real behaviour, returning none | optional | oauth | required. It adds no list to maintain: registering a provider the usual way is enough. oauth is deliberately NOT "works without a credential" — there is no key to paste, but signing in is still a barrier, and calling it keyless would mislead. anonymousApiKey outranks noAuth: AI Horde ships a documented anonymous key AND honours a real one for higher queue priority, so it is "optional" rather than "none" even though the form hides the field. Fixes one real inconsistency this branch introduced: ovhcloud was catalogued as keyless while its registry demanded a key. Verified live — the anonymous tier answers /chat/completions with no Authorization header, and a BAD key returns 403 instead of degrading, so authType is now "optional" and the executor attaches the header only when a real credential exists. The 10 pre-existing divergences (agy, blackbox, pollinations, puter, qwen-web, …) are frozen in KEYLESS_CATALOG_DRIFT with a stale-entry check: the gate blocks new drift, and fails if a frozen entry stops drifting so the debt list cannot outlive the debt. Resolving each one means confirming upstream behaviour, not editing a list.
Probing all ten providers the catalog labels `keyless` (2026-07-20) showed the label answers a different question than the UI was asking: blackbox 401 "No api key passed in." friendliai 401 "no authorization info provided" iflytek 401 Unauthorized sparkdesk 401 Unauthorized puter 401 "Missing authentication token" muse-spark-web 403 (authHeader is a session cookie, not a key) qwen-web 200 but serves the WAF HTML page, not the API liquid 404 — endpoint moved; needs its own audit pollinations 200 with real choices <- genuinely key-free ovhcloud 200, and 403 on a BAD key <- fixed earlier in this branch `freeType: "keyless"` means "free access not quantifiable in tokens" — it sits beside `oauth` in FREE_TIERS.md for exactly that reason. The new section was listing those rows under "No API key required", which would have sent users to providers that reject them. It now derives from getCredentialRequirement(). pollinations was the one real find: it answers with no credential at all, so its registry entry moves from apikey to optional and it leaves the recorded list. The list is computed in the route handler, not the component: deriving it client-side pulled the whole 201-entry provider REGISTRY into the browser bundle. The component takes `noCredentialProviders` from the payload and stays dumb — which is also why the vitest run could not resolve REGISTRY through the `@omniroute/*` alias and silently classified every provider as credentialed.
…live host
vitest.config.ts / vitest.mcp.config.ts had no `@omniroute/open-sse` alias, so
imports from open-sse resolved to undefined instead of throwing. Tests stayed
green while every lookup silently returned a default — that is how the free-tier
card asserted on provider credentials with REGISTRY never loaded. Both configs
now mirror the tsconfig paths, and tests/unit/ui/open-sse-alias.test.tsx pins it
by asserting on values only reachable through REGISTRY (aihorde's anonymous key,
pollinations' optional auth), so a future regression fails loudly.
liquid pointed at api.liquid.ai, which stopped serving the API — every path now
returns a Vercel 404 HTML page, so routing failed with an unparseable body
instead of a clean error. The live OpenAI-compatible host is inference.liquid.ai
(403 {"detail":"Not authenticated"} without a key). Both verified 2026-07-20.
Swept every free-catalog provider for the same failure. Five more looked dead on
a /models probe (agentrouter, coze, kiro, nlpcloud, puter) but answer their chat
endpoint with real API JSON — a 404 on /models only means the path is not
exposed. They are untouched: liquid was the only genuine casualty.
|
Reviewed #7840 end-to-end in an isolated worktree against Validated locally (all pre-existing, none of it PR-caused):
One gap worth closing before merge: Everything else — the anti-inflation discipline (no invented token quotas, single pooled row for navy instead of ~149× overcounting), the |
This PR adds one gateway provider (navy), so the frozen entry-count and the family-partition sum both shift by one. The assertions are moving targets by design — they exist to catch a provider silently landing in two families or in none, not to freeze the catalog size.
|
Validated in local merge-train on tomni-proxmox-113 @ 19bcf534352e24cdd2448cdfdfd64d5bd9695c74 (FAST gates green: static + changed tests + vitest) |
…g entries #7840 added navy/aihorde and moved liquid to inference.liquid.ai in providers.ts without regenerating tests/snapshots/provider/translate-path.json, leaving the golden gate (Unit shard 4/4) red on the release tip for every fresh PR run. Mechanical regen via UPDATE_GOLDEN=1; only those 3 entries change.
release/v3.8.49 tip took slot 129 via diegosouzapw#7843 (usage_history_codex_strong_identity, itself renumbered from 128 during the diegosouzapw#7838/diegosouzapw#7840 base-red cleanup) after this branch forked; renumber remove_unregistered_qwen_data to 130. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…rovider (#7866) * refactor(cli): remove legacy Qwen Code integration * refactor(qwen): remove deprecated Qwen OAuth provider * feat(cli): rebuild Qwen Code integration for upstream V4 * fix(qwen): clear stale CLI auth on reset * test(qwen): align retired provider coverage * fix(db): renumber qwen-cleanup migration 129 -> 130 release/v3.8.49 tip took slot 129 via #7843 (usage_history_codex_strong_identity, itself renumbered from 128 during the #7838/#7840 base-red cleanup) after this branch forked; renumber remove_unregistered_qwen_data to 130. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
* fix(dashboard): resolve Kimi banner casing collision + shrink frozen test file (release tip) - Rename src/app/(dashboard)/dashboard/kimiSponsorBanner.ts to kimiSponsorBannerGate.ts so it no longer differs from KimiSponsorBanner.tsx only by the first letter's case (breaks next build on case-insensitive filesystems). Updates the sole importer (KimiSponsorBanner.tsx) and the two tests that reference it. - Extract the 8 Kimi/Moonshot featured-ordering tests out of the frozen tests/unit/providers-page-utils.test.ts (grown 3 lines past its 1294 cap by #8039's rebrand-comment update) into a new sibling file tests/unit/providers-page-utils-kimi.test.ts. No assertions dropped; both files pass in full (24 + 8 = 32 tests). * fix(sse): register PromptQlExecutor in the executor registry (release tip) getExecutor("promptql") had no entry in open-sse/executors/index.ts, so it silently fell through to DefaultExecutor's provider fallback, which issues a raw fetch() and returns the bare upstream Response instead of the executor wrapper shape {response, url, headers, transformedBody}. The real PromptQlExecutor class (open-sse/executors/promptql.ts) already honors the contract correctly — it was just never wired into the registry. Fixes tests/unit/executor-web-cookie-sweep.test.ts "promptql executor returns wrapper shape". * fix(i18n): backfill 2220 missing pt-BR keys to restore en.json parity (release tip) pt-BR.json fell behind after #7935 restored +2220 keys into en.json and vi.json but left pt-BR.json unmodified. Translated all missing entries to Brazilian Portuguese, preserving ICU/interpolation placeholders and existing terminology, and merged them mirroring en.json's key order so the diff is additions-only (the small comma-only deletions are pure JSON reformatting from new sibling keys). * fix(providers): repair 4 pre-existing catalog/registry reds on release tip - providers-constants-split.test.ts: APIKEY_PROVIDERS grew 182->187 (PR #7887 added 5 free-tier providers: ainative/aion/sealion/routeway/nara). Verified no dup/loss (6-family partition sums exactly to 187) and updated the stale expected count + comment trail to match. - cline registry: added the missing minimax/minimax-m3 free OpenRouter entry (#3321) and fixed the neighbouring nemotron-3-ultra-550b-a55b entry, which carried a stray ":free" id suffix and an imprecise 1_000_000 contextLength instead of the 1_048_576 the test (and every sibling 1M-context entry in this catalog) expects. - promptqlModels.ts / registry/promptql/index.ts: PROMPTQL_FALLBACK_MODELS's minimax-m3 entry was missing supportsVision, and the registry mapping dropped it entirely (only id/name were passed through) — it was the sole minimax-m3 entry across the whole registry not flagged multimodal, despite every other provider (minimax, minimax-cn, ollama-cloud, trae, bazaarlink, clinepass, codebuddy-cn, opencode-zen/go, synthetic, huggingchat, lmarena) agreeing MiniMax-M3 supports vision. Added the field to the PromptQlModel type and threaded it through. - tests/snapshots/provider/translate-path.json: regenerated the golden via UPDATE_GOLDEN=1. Diffed old vs new — zero providers removed, 5 added (ainative/aion/nara/routeway/sealion, matching #7887), and the only changed entry (cline) reflects the already-merged #7914 ClinePass header protocol change (Cline/<version> User-Agent + X-Task-ID) that a prior narrow golden touch-up missed capturing. * fix(docs): repair docs-sync/env-sync/repo-contract gates (release tip) Six pre-existing reds on release/v3.8.49, all "repo drifted from its own documented contract": - check-docs-counts-sync: free-tier headline was stale (~1.4B/~2.0B) vs the live catalog (~1.53B steady / ~2.15B first month, 43 pools). Updated README.md and docs/reference/FREE_TIERS.md to the live numbers and added a v3.8.49 correction note explaining the pool-count delta (39->43, #7840). Also fixed a soft executors-count drift in ARCHITECTURE.md (84->86, 268->271 providers) while touching that line. - release-green-docs-drift-7253: docs/proxy-subscriptions.md referenced a fabricated migration filename (123_proxy_subscriptions.sql); the real file is 131_proxy_subscriptions.sql. Fixed all 3 occurrences. - check-env-doc-sync + issue-7793-env-doc-sync-repro: OMNIROUTE_DATA_DIR (DATA_DIR fallback alias read by open-sse/executors/promptql/threadSticky.ts) was undocumented. Added to .env.example and docs/reference/ENVIRONMENT.md. - check-db-rules: src/lib/db/proxySubscriptions.ts (#7299) is a db-internal split of proxies.ts (kept under the frozen file-size cap) whose one export is already re-exported via proxies.ts -> localDb.ts. Added it to INTENTIONALLY_INTERNAL with the same db-internal justification used for identical split modules (apiKeyColumnFallbacks, providerNodeSelect, webSessionDedup) rather than a redundant direct re-export from localDb.ts. - mcp-server-hollow-dist-deps: the sanity test expected better-sqlite3 among the MCP bundle's static top-level external imports. That's been stale since the pre-#7878 migration to a cascading SqliteAdapter driver factory (createRequire()-based lazy require, not a static import); better-sqlite3 already has its own native-asset copy guarantee in assembleStandalone.mjs, unrelated to this test's EXTRA_MODULE_ENTRIES concern. Updated the assertion to a still-genuinely-static external (zod) with a comment explaining the change. No production runtime behavior changed — docs, .env.example, and a checker allowlist/test-expectation only. * fix(dashboard): repair stale UI component-shape test assertions (release tip) Two pre-existing reds in the dashboard UI component-contract cluster were caused by test assertions that had gone stale after intentional, correct refactors — not by real defects in the components: - quota-pool-wizard-multi.test.ts: the step-3 preview assertion required the literal single-line substring "connectionIds.map((cid)". Prettier (100-char width, project config) legitimately breaks the connectionIds.map(...).filter(...) chain across lines because of the multi-line callback body, so the literal never matches. PoolWizard.tsx still builds previewByProvider correctly by mapping over connectionIds; updated the assertion to a regex that tolerates the line break. - v388-phase1-screen-fixes.test.ts: the shared Select placeholder-guard assertion required the literal "!children && placeholder". An earlier, intentional i18n commit changed the hardcoded "Select an option" default to a translated fallback (`placeholder ?? t("selectOption")`), which requires parens around the ?? expression for operator precedence. The guard behavior is unchanged (still gated on !children); updated the assertion to match the current, correct guard shape. Both fixes are read-only test-file changes; no production behavior changed. review-reviews-v3814-fixes.test.ts still has one pre-existing, unrelated red (LEDGER-4: minimax-m3 registry entries missing supportsVision) that requires editing the promptql provider registry/catalog — out of this cluster's scope, left untouched and reported separately. * fix(providers): reconcile cline catalog contradictions + deterministic golden (release tip) The first tip-green pass introduced 3 regressions caught by CI on sibling guard tests: - clinepass-provider + cline-catalog-models-3321 encoded OPPOSITE expectations of the same cline model list (minimax presence, nvidia :free suffix). Reference upstream (OpenRouter free lineup) confirms nvidia/nemotron-3-ultra-550b-a55b:free (with :free, 1M ctx) is correct, so restore that id and fix #3321's stale no-:free assertion; add minimax/minimax-m3 (the real #3321 gap) to clinepass-provider's list. - check-db-rules-classification froze INTENTIONALLY_INTERNAL at 35; proxySubscriptions was the intentional 36th entry — add it + bump the count. - provider-translate-path golden stored a LITERAL Cline/3.8.49: clineAuth resolves the version from APP_CONFIG.version (stable), but the golden sanitizer collapsed only process.env.npm_package_version (unset under `node`, set under `npm run`) — so the golden was shard-dependent. Resolve APP_VERSION from APP_CONFIG.version like clineAuth and regenerate; now Cline/<APP> normalizes identically in every shard. * fix(services): type execFile signal/killed in classifyError + ratchet dashboard baseline (release tip) Pre-existing base-red on the tip's Fast Quality Gates (dashboard-typecheck), missed in the first inventory: - src/lib/services/installers/utils.ts TS2339 — `err.signal` was read off a value typed as NodeJS.ErrnoException, which @types/node does not declare `signal`/`killed` on (those belong to execFile's ExecFileException). Widen classifyError's param to type both, and drop the now-redundant `(err as … { killed })` cast. - Ratchet config/quality/dashboard-typecheck-baseline.json down: 5 baselined errors were fixed by already-merged PRs but never ratcheted (OAuthModal TS2769 4→3 / TS2345 4→3, CliproxyModelMappingEditor TS2339, CompressionPreviewAccordion TS4104, MonacoEditor TS2307). Baseline now 254, matching live — gate exits 0.
…eyless providers (diegosouzapw#7840) * feat(catalog): map unmapped free tiers, add navy + aihorde, surface keyless Seven providers whose free tier was documented upstream but never reached our catalog. Five of them we could already route — only the quota was missing. Providers already routable, quota now mapped: - requesty (200 req/day), ovhcloud (2 req/min per IP, anonymous), agnes (permanently free), glm (GLM-4.7/4.5-Flash are Free on the official pricing table). All registered as recurring-uncapped: their free tier is capped in REQUESTS, not tokens, so inventing a token figure would inflate the headline. The "~30M/month" that circulates for GLM belongs to BigModel.cn (a separate Chinese offering) and is deliberately not recorded. New providers: - navy: one shared 150K tokens/day pool (~4.5M/month) drained by a per-model token_multiplier. Registered as a SINGLE pooled row — summing its ~149 free models would overcount ~149x. - aihorde: crowdsourced volunteer GPUs, keyless via the documented anonymous key. No tool calling and a 120s timeout, because requests queue for minutes. Also: - kilo-gateway reconciled against its live /models list (7 -> 13 models) and flagged with the new trainsOnPrompts field: every free Kilo model reports mayTrainOnYourPrompts: true, so the privacy cost now sits next to the quota. - Free-tier page gains search, provider/keyless filters, per-row type badges, a "no API key required" section and a curation-date freshness indicator. - catalogUpdatedAt comes from an explicit FREE_CATALOG_CURATED_AT constant rather than the data file's mtime: a standalone build rewrites timestamps on deploy, which would advertise a months-old catalog as updated today. Net effect on the headline: 462 -> 484 models but 1.371B -> 1.376B tokens, because only navy publishes a token quota. That is the point — coverage grows without the number lying. * refactor(providers): derive one answer for "does this need an API key?" "Works without a credential" lived in three registries that disagreed, and only three providers were classified the same way in all of them: - NOAUTH_PROVIDERS.noAuth -> whether the connect form hides the field - RegistryEntry.authType / anonymousApiKey -> what the executor really sends - FreeModelBudget.freeType === "keyless" -> how the catalog labels it getCredentialRequirement() now derives the answer from the two sources that describe real behaviour, returning none | optional | oauth | required. It adds no list to maintain: registering a provider the usual way is enough. oauth is deliberately NOT "works without a credential" — there is no key to paste, but signing in is still a barrier, and calling it keyless would mislead. anonymousApiKey outranks noAuth: AI Horde ships a documented anonymous key AND honours a real one for higher queue priority, so it is "optional" rather than "none" even though the form hides the field. Fixes one real inconsistency this branch introduced: ovhcloud was catalogued as keyless while its registry demanded a key. Verified live — the anonymous tier answers /chat/completions with no Authorization header, and a BAD key returns 403 instead of degrading, so authType is now "optional" and the executor attaches the header only when a real credential exists. The 10 pre-existing divergences (agy, blackbox, pollinations, puter, qwen-web, …) are frozen in KEYLESS_CATALOG_DRIFT with a stale-entry check: the gate blocks new drift, and fails if a frozen entry stops drifting so the debt list cannot outlive the debt. Resolving each one means confirming upstream behaviour, not editing a list. * fix(dashboard): build "no API key required" from routing, not freeType Probing all ten providers the catalog labels `keyless` (2026-07-20) showed the label answers a different question than the UI was asking: blackbox 401 "No api key passed in." friendliai 401 "no authorization info provided" iflytek 401 Unauthorized sparkdesk 401 Unauthorized puter 401 "Missing authentication token" muse-spark-web 403 (authHeader is a session cookie, not a key) qwen-web 200 but serves the WAF HTML page, not the API liquid 404 — endpoint moved; needs its own audit pollinations 200 with real choices <- genuinely key-free ovhcloud 200, and 403 on a BAD key <- fixed earlier in this branch `freeType: "keyless"` means "free access not quantifiable in tokens" — it sits beside `oauth` in FREE_TIERS.md for exactly that reason. The new section was listing those rows under "No API key required", which would have sent users to providers that reject them. It now derives from getCredentialRequirement(). pollinations was the one real find: it answers with no credential at all, so its registry entry moves from apikey to optional and it leaves the recorded list. The list is computed in the route handler, not the component: deriving it client-side pulled the whole 201-entry provider REGISTRY into the browser bundle. The component takes `noCredentialProviders` from the payload and stays dumb — which is also why the vitest run could not resolve REGISTRY through the `@omniroute/*` alias and silently classified every provider as credentialed. * fix(test,providers): resolve open-sse in vitest; point liquid at its live host vitest.config.ts / vitest.mcp.config.ts had no `@omniroute/open-sse` alias, so imports from open-sse resolved to undefined instead of throwing. Tests stayed green while every lookup silently returned a default — that is how the free-tier card asserted on provider credentials with REGISTRY never loaded. Both configs now mirror the tsconfig paths, and tests/unit/ui/open-sse-alias.test.tsx pins it by asserting on values only reachable through REGISTRY (aihorde's anonymous key, pollinations' optional auth), so a future regression fails loudly. liquid pointed at api.liquid.ai, which stopped serving the API — every path now returns a Vercel 404 HTML page, so routing failed with an unparseable body instead of a clean error. The live OpenAI-compatible host is inference.liquid.ai (403 {"detail":"Not authenticated"} without a key). Both verified 2026-07-20. Swept every free-catalog provider for the same failure. Five more looked dead on a /models probe (agentrouter, coze, kiro, nlpcloud, puter) but answer their chat endpoint with real API JSON — a 404 on /models only means the path is not exposed. They are untouched: liquid was the only genuine casualty. * test(providers): move the APIKEY_PROVIDERS partition count to 180 This PR adds one gateway provider (navy), so the frozen entry-count and the family-partition sum both shift by one. The assertions are moving targets by design — they exist to catch a provider silently landing in two families or in none, not to freeze the catalog size.
…#7840 catalog entries diegosouzapw#7840 added navy/aihorde and moved liquid to inference.liquid.ai in providers.ts without regenerating tests/snapshots/provider/translate-path.json, leaving the golden gate (Unit shard 4/4) red on the release tip for every fresh PR run. Mechanical regen via UPDATE_GOLDEN=1; only those 3 entries change.
…rovider (diegosouzapw#7866) * refactor(cli): remove legacy Qwen Code integration * refactor(qwen): remove deprecated Qwen OAuth provider * feat(cli): rebuild Qwen Code integration for upstream V4 * fix(qwen): clear stale CLI auth on reset * test(qwen): align retired provider coverage * fix(db): renumber qwen-cleanup migration 129 -> 130 release/v3.8.49 tip took slot 129 via diegosouzapw#7843 (usage_history_codex_strong_identity, itself renumbered from 128 during the diegosouzapw#7838/diegosouzapw#7840 base-red cleanup) after this branch forked; renumber remove_unregistered_qwen_data to 130. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
…ouzapw#8055) * fix(dashboard): resolve Kimi banner casing collision + shrink frozen test file (release tip) - Rename src/app/(dashboard)/dashboard/kimiSponsorBanner.ts to kimiSponsorBannerGate.ts so it no longer differs from KimiSponsorBanner.tsx only by the first letter's case (breaks next build on case-insensitive filesystems). Updates the sole importer (KimiSponsorBanner.tsx) and the two tests that reference it. - Extract the 8 Kimi/Moonshot featured-ordering tests out of the frozen tests/unit/providers-page-utils.test.ts (grown 3 lines past its 1294 cap by diegosouzapw#8039's rebrand-comment update) into a new sibling file tests/unit/providers-page-utils-kimi.test.ts. No assertions dropped; both files pass in full (24 + 8 = 32 tests). * fix(sse): register PromptQlExecutor in the executor registry (release tip) getExecutor("promptql") had no entry in open-sse/executors/index.ts, so it silently fell through to DefaultExecutor's provider fallback, which issues a raw fetch() and returns the bare upstream Response instead of the executor wrapper shape {response, url, headers, transformedBody}. The real PromptQlExecutor class (open-sse/executors/promptql.ts) already honors the contract correctly — it was just never wired into the registry. Fixes tests/unit/executor-web-cookie-sweep.test.ts "promptql executor returns wrapper shape". * fix(i18n): backfill 2220 missing pt-BR keys to restore en.json parity (release tip) pt-BR.json fell behind after diegosouzapw#7935 restored +2220 keys into en.json and vi.json but left pt-BR.json unmodified. Translated all missing entries to Brazilian Portuguese, preserving ICU/interpolation placeholders and existing terminology, and merged them mirroring en.json's key order so the diff is additions-only (the small comma-only deletions are pure JSON reformatting from new sibling keys). * fix(providers): repair 4 pre-existing catalog/registry reds on release tip - providers-constants-split.test.ts: APIKEY_PROVIDERS grew 182->187 (PR diegosouzapw#7887 added 5 free-tier providers: ainative/aion/sealion/routeway/nara). Verified no dup/loss (6-family partition sums exactly to 187) and updated the stale expected count + comment trail to match. - cline registry: added the missing minimax/minimax-m3 free OpenRouter entry (diegosouzapw#3321) and fixed the neighbouring nemotron-3-ultra-550b-a55b entry, which carried a stray ":free" id suffix and an imprecise 1_000_000 contextLength instead of the 1_048_576 the test (and every sibling 1M-context entry in this catalog) expects. - promptqlModels.ts / registry/promptql/index.ts: PROMPTQL_FALLBACK_MODELS's minimax-m3 entry was missing supportsVision, and the registry mapping dropped it entirely (only id/name were passed through) — it was the sole minimax-m3 entry across the whole registry not flagged multimodal, despite every other provider (minimax, minimax-cn, ollama-cloud, trae, bazaarlink, clinepass, codebuddy-cn, opencode-zen/go, synthetic, huggingchat, lmarena) agreeing MiniMax-M3 supports vision. Added the field to the PromptQlModel type and threaded it through. - tests/snapshots/provider/translate-path.json: regenerated the golden via UPDATE_GOLDEN=1. Diffed old vs new — zero providers removed, 5 added (ainative/aion/nara/routeway/sealion, matching diegosouzapw#7887), and the only changed entry (cline) reflects the already-merged diegosouzapw#7914 ClinePass header protocol change (Cline/<version> User-Agent + X-Task-ID) that a prior narrow golden touch-up missed capturing. * fix(docs): repair docs-sync/env-sync/repo-contract gates (release tip) Six pre-existing reds on release/v3.8.49, all "repo drifted from its own documented contract": - check-docs-counts-sync: free-tier headline was stale (~1.4B/~2.0B) vs the live catalog (~1.53B steady / ~2.15B first month, 43 pools). Updated README.md and docs/reference/FREE_TIERS.md to the live numbers and added a v3.8.49 correction note explaining the pool-count delta (39->43, diegosouzapw#7840). Also fixed a soft executors-count drift in ARCHITECTURE.md (84->86, 268->271 providers) while touching that line. - release-green-docs-drift-7253: docs/proxy-subscriptions.md referenced a fabricated migration filename (123_proxy_subscriptions.sql); the real file is 131_proxy_subscriptions.sql. Fixed all 3 occurrences. - check-env-doc-sync + issue-7793-env-doc-sync-repro: OMNIROUTE_DATA_DIR (DATA_DIR fallback alias read by open-sse/executors/promptql/threadSticky.ts) was undocumented. Added to .env.example and docs/reference/ENVIRONMENT.md. - check-db-rules: src/lib/db/proxySubscriptions.ts (diegosouzapw#7299) is a db-internal split of proxies.ts (kept under the frozen file-size cap) whose one export is already re-exported via proxies.ts -> localDb.ts. Added it to INTENTIONALLY_INTERNAL with the same db-internal justification used for identical split modules (apiKeyColumnFallbacks, providerNodeSelect, webSessionDedup) rather than a redundant direct re-export from localDb.ts. - mcp-server-hollow-dist-deps: the sanity test expected better-sqlite3 among the MCP bundle's static top-level external imports. That's been stale since the pre-diegosouzapw#7878 migration to a cascading SqliteAdapter driver factory (createRequire()-based lazy require, not a static import); better-sqlite3 already has its own native-asset copy guarantee in assembleStandalone.mjs, unrelated to this test's EXTRA_MODULE_ENTRIES concern. Updated the assertion to a still-genuinely-static external (zod) with a comment explaining the change. No production runtime behavior changed — docs, .env.example, and a checker allowlist/test-expectation only. * fix(dashboard): repair stale UI component-shape test assertions (release tip) Two pre-existing reds in the dashboard UI component-contract cluster were caused by test assertions that had gone stale after intentional, correct refactors — not by real defects in the components: - quota-pool-wizard-multi.test.ts: the step-3 preview assertion required the literal single-line substring "connectionIds.map((cid)". Prettier (100-char width, project config) legitimately breaks the connectionIds.map(...).filter(...) chain across lines because of the multi-line callback body, so the literal never matches. PoolWizard.tsx still builds previewByProvider correctly by mapping over connectionIds; updated the assertion to a regex that tolerates the line break. - v388-phase1-screen-fixes.test.ts: the shared Select placeholder-guard assertion required the literal "!children && placeholder". An earlier, intentional i18n commit changed the hardcoded "Select an option" default to a translated fallback (`placeholder ?? t("selectOption")`), which requires parens around the ?? expression for operator precedence. The guard behavior is unchanged (still gated on !children); updated the assertion to match the current, correct guard shape. Both fixes are read-only test-file changes; no production behavior changed. review-reviews-v3814-fixes.test.ts still has one pre-existing, unrelated red (LEDGER-4: minimax-m3 registry entries missing supportsVision) that requires editing the promptql provider registry/catalog — out of this cluster's scope, left untouched and reported separately. * fix(providers): reconcile cline catalog contradictions + deterministic golden (release tip) The first tip-green pass introduced 3 regressions caught by CI on sibling guard tests: - clinepass-provider + cline-catalog-models-3321 encoded OPPOSITE expectations of the same cline model list (minimax presence, nvidia :free suffix). Reference upstream (OpenRouter free lineup) confirms nvidia/nemotron-3-ultra-550b-a55b:free (with :free, 1M ctx) is correct, so restore that id and fix diegosouzapw#3321's stale no-:free assertion; add minimax/minimax-m3 (the real diegosouzapw#3321 gap) to clinepass-provider's list. - check-db-rules-classification froze INTENTIONALLY_INTERNAL at 35; proxySubscriptions was the intentional 36th entry — add it + bump the count. - provider-translate-path golden stored a LITERAL Cline/3.8.49: clineAuth resolves the version from APP_CONFIG.version (stable), but the golden sanitizer collapsed only process.env.npm_package_version (unset under `node`, set under `npm run`) — so the golden was shard-dependent. Resolve APP_VERSION from APP_CONFIG.version like clineAuth and regenerate; now Cline/<APP> normalizes identically in every shard. * fix(services): type execFile signal/killed in classifyError + ratchet dashboard baseline (release tip) Pre-existing base-red on the tip's Fast Quality Gates (dashboard-typecheck), missed in the first inventory: - src/lib/services/installers/utils.ts TS2339 — `err.signal` was read off a value typed as NodeJS.ErrnoException, which @types/node does not declare `signal`/`killed` on (those belong to execFile's ExecFileException). Widen classifyError's param to type both, and drop the now-redundant `(err as … { killed })` cast. - Ratchet config/quality/dashboard-typecheck-baseline.json down: 5 baselined errors were fixed by already-merged PRs but never ratcheted (OAuthModal TS2769 4→3 / TS2345 4→3, CliproxyModelMappingEditor TS2339, CompressionPreviewAccordion TS4104, MonacoEditor TS2307). Baseline now 254, matching live — gate exits 0.
…eyless providers (diegosouzapw#7840) * feat(catalog): map unmapped free tiers, add navy + aihorde, surface keyless Seven providers whose free tier was documented upstream but never reached our catalog. Five of them we could already route — only the quota was missing. Providers already routable, quota now mapped: - requesty (200 req/day), ovhcloud (2 req/min per IP, anonymous), agnes (permanently free), glm (GLM-4.7/4.5-Flash are Free on the official pricing table). All registered as recurring-uncapped: their free tier is capped in REQUESTS, not tokens, so inventing a token figure would inflate the headline. The "~30M/month" that circulates for GLM belongs to BigModel.cn (a separate Chinese offering) and is deliberately not recorded. New providers: - navy: one shared 150K tokens/day pool (~4.5M/month) drained by a per-model token_multiplier. Registered as a SINGLE pooled row — summing its ~149 free models would overcount ~149x. - aihorde: crowdsourced volunteer GPUs, keyless via the documented anonymous key. No tool calling and a 120s timeout, because requests queue for minutes. Also: - kilo-gateway reconciled against its live /models list (7 -> 13 models) and flagged with the new trainsOnPrompts field: every free Kilo model reports mayTrainOnYourPrompts: true, so the privacy cost now sits next to the quota. - Free-tier page gains search, provider/keyless filters, per-row type badges, a "no API key required" section and a curation-date freshness indicator. - catalogUpdatedAt comes from an explicit FREE_CATALOG_CURATED_AT constant rather than the data file's mtime: a standalone build rewrites timestamps on deploy, which would advertise a months-old catalog as updated today. Net effect on the headline: 462 -> 484 models but 1.371B -> 1.376B tokens, because only navy publishes a token quota. That is the point — coverage grows without the number lying. * refactor(providers): derive one answer for "does this need an API key?" "Works without a credential" lived in three registries that disagreed, and only three providers were classified the same way in all of them: - NOAUTH_PROVIDERS.noAuth -> whether the connect form hides the field - RegistryEntry.authType / anonymousApiKey -> what the executor really sends - FreeModelBudget.freeType === "keyless" -> how the catalog labels it getCredentialRequirement() now derives the answer from the two sources that describe real behaviour, returning none | optional | oauth | required. It adds no list to maintain: registering a provider the usual way is enough. oauth is deliberately NOT "works without a credential" — there is no key to paste, but signing in is still a barrier, and calling it keyless would mislead. anonymousApiKey outranks noAuth: AI Horde ships a documented anonymous key AND honours a real one for higher queue priority, so it is "optional" rather than "none" even though the form hides the field. Fixes one real inconsistency this branch introduced: ovhcloud was catalogued as keyless while its registry demanded a key. Verified live — the anonymous tier answers /chat/completions with no Authorization header, and a BAD key returns 403 instead of degrading, so authType is now "optional" and the executor attaches the header only when a real credential exists. The 10 pre-existing divergences (agy, blackbox, pollinations, puter, qwen-web, …) are frozen in KEYLESS_CATALOG_DRIFT with a stale-entry check: the gate blocks new drift, and fails if a frozen entry stops drifting so the debt list cannot outlive the debt. Resolving each one means confirming upstream behaviour, not editing a list. * fix(dashboard): build "no API key required" from routing, not freeType Probing all ten providers the catalog labels `keyless` (2026-07-20) showed the label answers a different question than the UI was asking: blackbox 401 "No api key passed in." friendliai 401 "no authorization info provided" iflytek 401 Unauthorized sparkdesk 401 Unauthorized puter 401 "Missing authentication token" muse-spark-web 403 (authHeader is a session cookie, not a key) qwen-web 200 but serves the WAF HTML page, not the API liquid 404 — endpoint moved; needs its own audit pollinations 200 with real choices <- genuinely key-free ovhcloud 200, and 403 on a BAD key <- fixed earlier in this branch `freeType: "keyless"` means "free access not quantifiable in tokens" — it sits beside `oauth` in FREE_TIERS.md for exactly that reason. The new section was listing those rows under "No API key required", which would have sent users to providers that reject them. It now derives from getCredentialRequirement(). pollinations was the one real find: it answers with no credential at all, so its registry entry moves from apikey to optional and it leaves the recorded list. The list is computed in the route handler, not the component: deriving it client-side pulled the whole 201-entry provider REGISTRY into the browser bundle. The component takes `noCredentialProviders` from the payload and stays dumb — which is also why the vitest run could not resolve REGISTRY through the `@omniroute/*` alias and silently classified every provider as credentialed. * fix(test,providers): resolve open-sse in vitest; point liquid at its live host vitest.config.ts / vitest.mcp.config.ts had no `@omniroute/open-sse` alias, so imports from open-sse resolved to undefined instead of throwing. Tests stayed green while every lookup silently returned a default — that is how the free-tier card asserted on provider credentials with REGISTRY never loaded. Both configs now mirror the tsconfig paths, and tests/unit/ui/open-sse-alias.test.tsx pins it by asserting on values only reachable through REGISTRY (aihorde's anonymous key, pollinations' optional auth), so a future regression fails loudly. liquid pointed at api.liquid.ai, which stopped serving the API — every path now returns a Vercel 404 HTML page, so routing failed with an unparseable body instead of a clean error. The live OpenAI-compatible host is inference.liquid.ai (403 {"detail":"Not authenticated"} without a key). Both verified 2026-07-20. Swept every free-catalog provider for the same failure. Five more looked dead on a /models probe (agentrouter, coze, kiro, nlpcloud, puter) but answer their chat endpoint with real API JSON — a 404 on /models only means the path is not exposed. They are untouched: liquid was the only genuine casualty. * test(providers): move the APIKEY_PROVIDERS partition count to 180 This PR adds one gateway provider (navy), so the frozen entry-count and the family-partition sum both shift by one. The assertions are moving targets by design — they exist to catch a provider silently landing in two families or in none, not to freeze the catalog size.
…#7840 catalog entries diegosouzapw#7840 added navy/aihorde and moved liquid to inference.liquid.ai in providers.ts without regenerating tests/snapshots/provider/translate-path.json, leaving the golden gate (Unit shard 4/4) red on the release tip for every fresh PR run. Mechanical regen via UPDATE_GOLDEN=1; only those 3 entries change.
…rovider (diegosouzapw#7866) * refactor(cli): remove legacy Qwen Code integration * refactor(qwen): remove deprecated Qwen OAuth provider * feat(cli): rebuild Qwen Code integration for upstream V4 * fix(qwen): clear stale CLI auth on reset * test(qwen): align retired provider coverage * fix(db): renumber qwen-cleanup migration 129 -> 130 release/v3.8.49 tip took slot 129 via diegosouzapw#7843 (usage_history_codex_strong_identity, itself renumbered from 128 during the diegosouzapw#7838/diegosouzapw#7840 base-red cleanup) after this branch forked; renumber remove_unregistered_qwen_data to 130. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
…ouzapw#8055) * fix(dashboard): resolve Kimi banner casing collision + shrink frozen test file (release tip) - Rename src/app/(dashboard)/dashboard/kimiSponsorBanner.ts to kimiSponsorBannerGate.ts so it no longer differs from KimiSponsorBanner.tsx only by the first letter's case (breaks next build on case-insensitive filesystems). Updates the sole importer (KimiSponsorBanner.tsx) and the two tests that reference it. - Extract the 8 Kimi/Moonshot featured-ordering tests out of the frozen tests/unit/providers-page-utils.test.ts (grown 3 lines past its 1294 cap by diegosouzapw#8039's rebrand-comment update) into a new sibling file tests/unit/providers-page-utils-kimi.test.ts. No assertions dropped; both files pass in full (24 + 8 = 32 tests). * fix(sse): register PromptQlExecutor in the executor registry (release tip) getExecutor("promptql") had no entry in open-sse/executors/index.ts, so it silently fell through to DefaultExecutor's provider fallback, which issues a raw fetch() and returns the bare upstream Response instead of the executor wrapper shape {response, url, headers, transformedBody}. The real PromptQlExecutor class (open-sse/executors/promptql.ts) already honors the contract correctly — it was just never wired into the registry. Fixes tests/unit/executor-web-cookie-sweep.test.ts "promptql executor returns wrapper shape". * fix(i18n): backfill 2220 missing pt-BR keys to restore en.json parity (release tip) pt-BR.json fell behind after diegosouzapw#7935 restored +2220 keys into en.json and vi.json but left pt-BR.json unmodified. Translated all missing entries to Brazilian Portuguese, preserving ICU/interpolation placeholders and existing terminology, and merged them mirroring en.json's key order so the diff is additions-only (the small comma-only deletions are pure JSON reformatting from new sibling keys). * fix(providers): repair 4 pre-existing catalog/registry reds on release tip - providers-constants-split.test.ts: APIKEY_PROVIDERS grew 182->187 (PR diegosouzapw#7887 added 5 free-tier providers: ainative/aion/sealion/routeway/nara). Verified no dup/loss (6-family partition sums exactly to 187) and updated the stale expected count + comment trail to match. - cline registry: added the missing minimax/minimax-m3 free OpenRouter entry (diegosouzapw#3321) and fixed the neighbouring nemotron-3-ultra-550b-a55b entry, which carried a stray ":free" id suffix and an imprecise 1_000_000 contextLength instead of the 1_048_576 the test (and every sibling 1M-context entry in this catalog) expects. - promptqlModels.ts / registry/promptql/index.ts: PROMPTQL_FALLBACK_MODELS's minimax-m3 entry was missing supportsVision, and the registry mapping dropped it entirely (only id/name were passed through) — it was the sole minimax-m3 entry across the whole registry not flagged multimodal, despite every other provider (minimax, minimax-cn, ollama-cloud, trae, bazaarlink, clinepass, codebuddy-cn, opencode-zen/go, synthetic, huggingchat, lmarena) agreeing MiniMax-M3 supports vision. Added the field to the PromptQlModel type and threaded it through. - tests/snapshots/provider/translate-path.json: regenerated the golden via UPDATE_GOLDEN=1. Diffed old vs new — zero providers removed, 5 added (ainative/aion/nara/routeway/sealion, matching diegosouzapw#7887), and the only changed entry (cline) reflects the already-merged diegosouzapw#7914 ClinePass header protocol change (Cline/<version> User-Agent + X-Task-ID) that a prior narrow golden touch-up missed capturing. * fix(docs): repair docs-sync/env-sync/repo-contract gates (release tip) Six pre-existing reds on release/v3.8.49, all "repo drifted from its own documented contract": - check-docs-counts-sync: free-tier headline was stale (~1.4B/~2.0B) vs the live catalog (~1.53B steady / ~2.15B first month, 43 pools). Updated README.md and docs/reference/FREE_TIERS.md to the live numbers and added a v3.8.49 correction note explaining the pool-count delta (39->43, diegosouzapw#7840). Also fixed a soft executors-count drift in ARCHITECTURE.md (84->86, 268->271 providers) while touching that line. - release-green-docs-drift-7253: docs/proxy-subscriptions.md referenced a fabricated migration filename (123_proxy_subscriptions.sql); the real file is 131_proxy_subscriptions.sql. Fixed all 3 occurrences. - check-env-doc-sync + issue-7793-env-doc-sync-repro: OMNIROUTE_DATA_DIR (DATA_DIR fallback alias read by open-sse/executors/promptql/threadSticky.ts) was undocumented. Added to .env.example and docs/reference/ENVIRONMENT.md. - check-db-rules: src/lib/db/proxySubscriptions.ts (diegosouzapw#7299) is a db-internal split of proxies.ts (kept under the frozen file-size cap) whose one export is already re-exported via proxies.ts -> localDb.ts. Added it to INTENTIONALLY_INTERNAL with the same db-internal justification used for identical split modules (apiKeyColumnFallbacks, providerNodeSelect, webSessionDedup) rather than a redundant direct re-export from localDb.ts. - mcp-server-hollow-dist-deps: the sanity test expected better-sqlite3 among the MCP bundle's static top-level external imports. That's been stale since the pre-diegosouzapw#7878 migration to a cascading SqliteAdapter driver factory (createRequire()-based lazy require, not a static import); better-sqlite3 already has its own native-asset copy guarantee in assembleStandalone.mjs, unrelated to this test's EXTRA_MODULE_ENTRIES concern. Updated the assertion to a still-genuinely-static external (zod) with a comment explaining the change. No production runtime behavior changed — docs, .env.example, and a checker allowlist/test-expectation only. * fix(dashboard): repair stale UI component-shape test assertions (release tip) Two pre-existing reds in the dashboard UI component-contract cluster were caused by test assertions that had gone stale after intentional, correct refactors — not by real defects in the components: - quota-pool-wizard-multi.test.ts: the step-3 preview assertion required the literal single-line substring "connectionIds.map((cid)". Prettier (100-char width, project config) legitimately breaks the connectionIds.map(...).filter(...) chain across lines because of the multi-line callback body, so the literal never matches. PoolWizard.tsx still builds previewByProvider correctly by mapping over connectionIds; updated the assertion to a regex that tolerates the line break. - v388-phase1-screen-fixes.test.ts: the shared Select placeholder-guard assertion required the literal "!children && placeholder". An earlier, intentional i18n commit changed the hardcoded "Select an option" default to a translated fallback (`placeholder ?? t("selectOption")`), which requires parens around the ?? expression for operator precedence. The guard behavior is unchanged (still gated on !children); updated the assertion to match the current, correct guard shape. Both fixes are read-only test-file changes; no production behavior changed. review-reviews-v3814-fixes.test.ts still has one pre-existing, unrelated red (LEDGER-4: minimax-m3 registry entries missing supportsVision) that requires editing the promptql provider registry/catalog — out of this cluster's scope, left untouched and reported separately. * fix(providers): reconcile cline catalog contradictions + deterministic golden (release tip) The first tip-green pass introduced 3 regressions caught by CI on sibling guard tests: - clinepass-provider + cline-catalog-models-3321 encoded OPPOSITE expectations of the same cline model list (minimax presence, nvidia :free suffix). Reference upstream (OpenRouter free lineup) confirms nvidia/nemotron-3-ultra-550b-a55b:free (with :free, 1M ctx) is correct, so restore that id and fix diegosouzapw#3321's stale no-:free assertion; add minimax/minimax-m3 (the real diegosouzapw#3321 gap) to clinepass-provider's list. - check-db-rules-classification froze INTENTIONALLY_INTERNAL at 35; proxySubscriptions was the intentional 36th entry — add it + bump the count. - provider-translate-path golden stored a LITERAL Cline/3.8.49: clineAuth resolves the version from APP_CONFIG.version (stable), but the golden sanitizer collapsed only process.env.npm_package_version (unset under `node`, set under `npm run`) — so the golden was shard-dependent. Resolve APP_VERSION from APP_CONFIG.version like clineAuth and regenerate; now Cline/<APP> normalizes identically in every shard. * fix(services): type execFile signal/killed in classifyError + ratchet dashboard baseline (release tip) Pre-existing base-red on the tip's Fast Quality Gates (dashboard-typecheck), missed in the first inventory: - src/lib/services/installers/utils.ts TS2339 — `err.signal` was read off a value typed as NodeJS.ErrnoException, which @types/node does not declare `signal`/`killed` on (those belong to execFile's ExecFileException). Widen classifyError's param to type both, and drop the now-redundant `(err as … { killed })` cast. - Ratchet config/quality/dashboard-typecheck-baseline.json down: 5 baselined errors were fixed by already-merged PRs but never ratcheted (OAuthModal TS2769 4→3 / TS2345 4→3, CliproxyModelMappingEditor TS2339, CompressionPreviewAccordion TS4104, MonacoEditor TS2307). Baseline now 254, matching live — gate exits 0.
What
Seven providers whose free tier is documented upstream but never reached our catalog. Five of them we could already route — only the quota was missing (
requestyeven carriedhasFree: truewith the text "Free tier ~200 requests/day" and had zero catalog entries).Providers already routable — quota now mapped
requestyovhcloudagnesglmAll four registered as
recurring-uncapped: their free tier is capped in requests, not tokens. Inventing a token figure is how a catalog starts lying, so they stay visible viauncappedProviderswithout touching the headline.The "~30M/month" figure circulating for GLM belongs to BigModel.cn (a separate Chinese offering), not the global
api.z.aitier — deliberately not recorded.New providers
navy— one shared pool of 150K tokens/day (~4.5M/month), drained by a per-modeltoken_multiplierits public/v1/modelsexposes. Registered as a single pooled row: summing its ~149 free models would overcount ~149×. Real capacity varies sharply —llama-3.3-70b(1×) gets the full 150K/day,grok-4(10×) only ~15K.aihorde— crowdsourced volunteer GPUs, keyless via the documented anonymous key0000000000(reusing theanonymousApiKeyhook added for Kilo in #4019). Requests queue for minutes, so it carries a 120s timeout and no tool calling.Also
kilo-gatewayreconciled against its live/modelslist: 7 → 13 models (poolside/laguna-xs.2had been renamed,nex-agi/nex-n2-prodropped off the free list).trainsOnPromptsfield. Kilo's public catalog reportsmayTrainOnYourPrompts: trueon 13 of 13 free models. The privacy cost of a "free" tier now sits next to the quota instead of being invisible.catalogUpdatedAtcomes from an explicitFREE_CATALOG_CURATED_ATconstant, not the data file's mtime — a standalone build rewrites timestamps on every deploy, which would advertise a months-old catalog as "updated today".Net effect
462 → 484 models, but 1.371B → 1.376B tokens. Only
navypublishes a token quota; everything else is request-capped or a queue. Coverage grows without the number lying — the same pool-dedupe disciplinecomputeFreeModelTotals()already enforces.Test plan
tests/unit/free-catalog-2026-07-expansion.test.ts(new, 6 tests) pins each decision that is easy to silently undo: navy stays one pooled row; ovhcloud/aihorde stay keyless; request-capped providers never carry invented token numbers; every Kilo free model keeps the training flag; new providers resolve in both REGISTRY and AI_PROVIDERS.tests/unit/ui/free-budget-card.test.tsx— the previous card test was a dead orphan: it lived attests/unit/root, so no runner ever collected it (test:unitexcludes.tsx; onlytests/unit/ui/**is wired into vitest). Moved into the collected directory and grown from 5 never-run cases to 17 passing ones; the now-stale discovery-baseline entry was removed.typecheck:core,eslinton every touched file,check:provider-consistency(201 REGISTRY entries),check:file-size,check:known-symbols,check:test-discovery,check:docs-sync,free-model-catalog(6/6),free-tier-summary-route(5/5), UI card (17/17).origin/release/v3.8.49worktree and unrelated to this change:combos-page-smoke,evals-tab-smoke,lobe-provider-icons-stepfun.Second commit — one answer for "does this need an API key?"
"Works without a credential" lived in three registries that disagreed. Of 28 providers touching the question, only three were classified the same way in all three:
NOAUTH_PROVIDERS.noAuthRegistryEntry.authType/anonymousApiKeyFreeModelBudget.freeType === "keyless"getCredentialRequirement()derives the answer from the two sources that describe real behaviour, returningnone | optional | oauth | required. It adds no list to maintain — registering a provider the usual way is enough.Two judgement calls worth reviewing:
oauthis deliberately NOT "works without a credential". There is no key to paste, but signing in is still a barrier; calling it keyless would mislead the user reading the "No API key required" section.anonymousApiKeyoutranksnoAuth. AI Horde ships a documented anonymous key and honours a real one for higher queue priority — so it isoptional, notnone, even though the form hides the field.Fixes an inconsistency this branch introduced
ovhcloudwas catalogued askeylesswhile its registry demanded a key — the catalog promising what routing could not deliver. Verified live: the anonymous tier answers/chat/completionswith noAuthorizationheader (HTTP 200), and a bad key returns 403 instead of degrading. SoauthTypeis now"optional"and the executor attaches the header only when a real credential exists.The remaining 10 are frozen, not hidden
agy,blackbox,friendliai,iflytek,liquid,muse-spark-web,pollinations,puter,qwen-web,sparkdeskare pre-existing divergences kept inKEYLESS_CATALOG_DRIFT. The gate blocks new drift and also fails when a frozen entry stops drifting, so the debt list cannot outlive the debt. Resolving each means confirming upstream behaviour, not editing a list.tests/unit/provider-credential-requirement.test.ts(5 tests) pins the classification of each credential model, that unknown ids fail closed, and both directions of the allowlist.