Five remaining items from the May 2026 audit:
#15: Tightened integration test catalog count from >=14 to >=50 — current
catalog post-V9 is 60+ rows. Loose threshold would silently miss a future
migration that drops a chunk.
#17: Added migration-idempotency test
(server/src/__tests__/db/idempotency.test.ts):
- initDb -> close -> initDb on the same file produces identical row counts.
- Every catalog row has exactly one fallback_config entry.
- UNIQUE(platform, model_id) holds — no duplicate rows.
- Every enabled platform has a registered provider.
#19: crypto.ts now validates ENCRYPTION_KEY length and hex-format at
init time. A typo'd key like 'abc' previously fell through and only failed
on the first encrypt() with a cryptic node:crypto error. Now throws fast
with an actionable message including the command to generate a valid key.
Same validation applies to the DB-persisted key path. New test file
crypto-init.test.ts covers all paths (valid, too-short, too-long, non-hex
env key; cor…
chore: remove dead provider classes + fix orphan test fixtures + gitignore .bak (#25)
Janitorial cleanup from the May 2026 audit:
1. Deleted server/src/providers/groq.ts and cerebras.ts (and their tests).
Both were unreachable code: providers/index.ts has registered Groq and
Cerebras via the generic OpenAICompatProvider since at least V4. The
dedicated subclasses were only instantiated by their own tests.
2. Updated openai-compat.test.ts platform-instance fixture:
- Dropped 'fireworks' row — Fireworks AI was never a registered platform.
Test was passing only because of an 'as any' cast that silenced the
Platform type error.
- Updated 'github' baseUrl from the legacy 'models.inference.ai.azure.com'
to the current 'models.github.ai/inference' (matches the actual
registration after PR #18).
- Added rows for 'groq', 'cerebras', and 'zhipu' so the fixture mirrors
the live provider registry. Future drift between fixture and live
registry will fail loudly.
3. Added '*.bak' to .gitignore — '*.db' covers freeapi.db.bak today
…
fix(catalog): 400 on explicit unknown/disabled model + sync docs (#24)
Two cleanups from the May 2026 audit:
1. /v1/chat/completions — when the request includes an explicit `model`
field that doesn't resolve to an enabled catalog row, return 400
instead of silently auto-routing to a totally different model. The
silent fallthrough surprised playground users who 'forced' a disabled
model and got something unrelated back without a signal.
Behavior:
- model omitted → auto-route via fallback (unchanged).
- model matches enabled row → pin to it (unchanged).
- model matches disabled row → 400 with reason 'is disabled'.
- model not in catalog at all → 400 with reason 'is not in the catalog'.
Both 400s carry error.code = 'model_not_found'.
2. docs/index.html stale references — removed the Hugging Face provider
card (HF was dropped from the catalog in V4), updated provider count
from 12 → 11 across meta description, OG description, Twitter card,
hero badge, and tokens-stat sub. Removed the orphan logos/huggingface.svg.
Ve…
Six related fixes from the recent codebase audit:
1. Auth bypass on /v1/chat/completions — guard was 'if (authHeader && !isLocal)'
so any non-local caller that simply omitted the Authorization header skipped
the check entirely. Server binds 0.0.0.0; this exposed the proxy to any LAN
peer. Fix: 'if (!isLocal)' always require a valid Bearer for non-local.
2. Health-check auto-disable conflated network errors with bad keys. Three
transient timeouts (15 min) silently flipped enabled=0 on perfectly valid
keys (burned us this session). Fix:
- validateKey() in OpenAICompatProvider/google/cohere/cloudflare no longer
swallows transport errors — they propagate.
- health.ts catch branch sets status='error' but does NOT increment the
consecutive-failure counter. Only confirmed 401/403 (validateKey returns
false) counts toward auto-disable.
3. Removed Hugging Face / Moonshot / MiniMax direct integrations from the
live surface (PLATFORMS allowlist, providers…
Cerebras's /v1/models still lists the model, but chat-completions
returns 404 "Model does not exist or you do not have access" for
free-tier keys (re-confirmed after a 60-model end-to-end sweep).
Matches Cerebras's docs note about temporarily reducing free-tier
access on zai-glm-4.7 due to high demand.
Sets enabled = 0 on the row rather than deleting it, so fallback_config
history is preserved for when access returns. All 75 server tests pass.
fix(github): route GitHub Models via models.github.ai/inference (#18)
* fix(proxy): honor explicit model field in chat-completions requests
The schema parsed `model` but the destructure dropped it, so
`routeRequest` only ever saw the sticky-session fallback. Effect:
the playground's model picker (and any OpenAI SDK client passing
`model`) was silently ignored — every request landed on whatever
was top of fallback_config.
Fix: when `model` is present, look it up in the catalog
(`models WHERE model_id = ? AND enabled = 1`) and pass its db id
as `preferredModelDbId`. Sticky-session is preserved as a fallback
for multi-turn flows that don't re-send the model field.
Unknown / disabled models silently fall through to the auto-route
chain rather than 400ing — keeps behavior compatible with SDKs that
hardcode foreign model names like 'gpt-4o'.
* fix(github): route GitHub Models via models.github.ai/inference
The provider was pointed at the legacy Azure endpoint
(`models.inference.ai.azure.com`), which rejects the new
publisher-prefixed model ids the catalog…
* fix(server): guard errorHandler against headers-already-sent
When an LLM completion errors mid-stream, the response is already
flushing tokens to the client. The handler then unconditionally called
res.status().json(), throwing ERR_HTTP_HEADERS_SENT and triggering a
pm2 restart. Short-circuit to Express's default handler once headers
have been sent so the socket closes cleanly.
* feat(providers): configurable per-provider HTTP timeout
OpenAICompatProvider now accepts an optional timeoutMs constructor
option (default 15000ms). Cloud APIs respond well within the existing
default, but locally-hosted OpenAI-compatible inference (llama.cpp,
vLLM on CPU) can take 30-120s for long prompts and was being aborted
mid-generation, causing the proxy to mark the key invalid.
* fix(keys): add zhipu, moonshot, minimax to platform allowlist
These three platforms exist in the Platform type union and have
provider registrations, but were missing from the PLATFORMS array
in the keys route. Without …
feat(catalog): migrateModelsV6 — probe-verified additions and Google RPD fix (#6)
Live-probed against real free-tier keys on 2026-04-25. Adds 8 models
that returned 200 with content, drops the one OR :free route that
404s, and corrects two Google rate-limits whose catalog values were
~10x-50x too high.
Adds:
- Cloudflare: @cf/moonshotai/kimi-k2.5, @cf/qwen/qwen3-30b-a3b-fp8,
@cf/deepseek-ai/deepseek-r1-distill-qwen-32b
- Google preview: gemini-3-flash-preview, gemini-3.1-flash-lite-preview,
gemini-3.1-pro-preview (Pro confirmed free-tier-eligible by the
free_tier_requests quota metric in 429 errors)
- OpenRouter: google/gemma-4-31b-it:free, liquid/lfm-2.5-1.2b-instruct:free
Removes:
- openrouter/arcee-ai/trinity-large-preview:free (404 No endpoints found)
Corrects:
- gemini-2.5-flash and gemini-2.5-flash-lite RPD 250/1000 -> 20.
Free tier now uniformly enforces 20 RPD per model per project.
Updates router test rationale: gemini-3.1-pro-preview at rank 1 now
outranks Groq's gpt-oss-120b (rank 6) when keys exist for both.
Google moved Pro-tier Gemini off the free tier on 2026-04-01. Cerebras
added z.ai GLM-4.7 (355B) to their free tier, throttled to 10 RPM /
100 RPD with an 8192 context cap while demand stays high.
fix(cloudflare): normalize null content to empty string for tool round-trips
Cloudflare's OpenAI-compat endpoint rejects assistant messages with
content: null, even when tool_calls are present (standard OpenAI format).
Added normalizeMessages() that converts null content to "" before dispatch,
plus a regression test covering the null-content + tool_calls case.
Also credits @moaaz12-web in README Contributors for the tool-calling PR.
Kimi, Gemma, M1, HF depart the stage,
DeepSeek, Maverick, Ling, and GPT-OSS engage.
Twenty-two new rows, four fade from view —
Agentic rank reshapes what falls through.