Mark duckduckgo-web in WEB_COOKIE_PROVIDERS with noAuth and include it in providerAllowsOptionalApiKey. Update getProviderCredentials to check both NOAUTH_PROVIDERS and WEB_COOKIE_PROVIDERS for noAuth providers (returning synthetic credentials) and import WEB_COOKIE_PROVIDERS accordingly. This enables anonymous/anonymous-cookie web providers to be handled without DB lookups.
Adds 17 missing exports to the __testing object in usage.ts so
usage-utils.test.ts can import and test them. Also adds the
test file itself.
- toDisplayLabel, getClaudePlanLabel, createQuotaFromUsage
- getMiniMaxQuotaResetAt, isMiniMaxTextQuotaModel
- getMiniMaxSessionTotal, getMiniMaxWeeklyTotal
- createMiniMaxQuotaFromCount, getMiniMaxAuthErrorMessage
- getMiniMaxErrorSummary
- mapCodeAssistSubscriptionToPlanLabel
- mapCodeAssistTierIdToLabel
- mapSubscriptionTierStringToPlanLabel
Payload rules are written to the DB (key_value settings.payloadRules) and
mirrored into an in-memory runtimeOverride. After a restart, if runtimeOverride
is null — the boot applyRuntimeSettings hook didn't run in this module instance,
or a separate bundle instance in the standalone Next.js build — getPayloadRulesConfig
fell back to the (usually empty) config/payloadRules.json file and returned no
rules, so saved rules appeared to vanish.
The persistence/apply path is correct on inspection (DB write+read both use the
'settings' namespace; the boot hook sets the override with force:true); the null
override is an environment/bundling effect. Make getPayloadRulesConfig read the
DB-persisted rules (via getCachedSettings, the source of truth) before the file
when no override is set, so saved rules deterministically survive a restart.
Closes#2986
Custom models (added via the UI) on opencode-go / openai-compatible nodes always
routed as OpenAI-compatible because there was no per-model targetFormat: addCustomModel
didn't accept it, the API schema stripped it, and getModelTargetFormat (static-registry
only) never saw it — so a custom model needing the Anthropic Messages shape fell back to
the provider default 'openai'.
Thread an optional targetFormat through addCustomModel / replaceCustomModels /
updateCustomModel + the providerModelMutationSchema + the POST/PUT route, surface it from
getModelInfo (one combined custom-model metadata lookup alongside apiFormat), and use it
in chatCore's targetFormat resolution before the provider default.
Closes#2905
Pollinations retired the legacy text.pollinations.ai host, which now returns
404 'This is our legacy API' for all models. The registry primary baseUrl (and
the executor's hardcoded fallback) still pointed at it; gen.pollinations.ai/v1
is the current OpenAI-compatible gateway (it was already listed as the
secondary fallback). Make gen the sole/primary endpoint and align the executor
test.
Closes#2987
Free-plan Codex accounts (workspacePlanType === "free", from the OAuth id_token)
cannot run the server-side image_generation hosted tool, but the Codex CLI
injects it into every Responses request — causing an upstream 400. normalizeCodexTools
now drops image_generation when the connection is a free-plan account; paid plans
keep it. Mirrors CLIProxyAPI's isCodexFreePlanAuth guard.
Found during the #2980 (Codex OAuth compat) analysis against CLIProxyAPI.
Custom providers (openai-compatible-<uuid> / anthropic-compatible-<uuid>) showed
the raw UUID id instead of the user-given node name in the active-requests panel,
proxy logger, and home-page provider topology. Only RequestLoggerV2 resolved it
(via a local helper + /api/provider-nodes fetch).
Extract the resolver into a shared util (src/shared/utils/providerDisplayLabel.ts,
unit-tested), reuse it in RequestLoggerV2, and apply it (with a provider-nodes
fetch) in ActiveRequestsPanel, ProxyLogger, and the HomePageClient topology so all
surfaces show the user-defined provider name.
Closes#2968
The Docker image bakes NODE_OPTIONS=--max-old-space-size=256, and the standalone
launcher (scripts/dev/run-standalone.mjs, the Docker CMD) spawned 'node
server.js' without overriding it — so the server inherited the 256 MB cap and
OOMed randomly under load or with a large SQLite DB. `omniroute serve` already
honored OMNIROUTE_MEMORY_MB but the Docker path did not.
Add a shared resolveMaxOldSpaceMb() helper (OMNIROUTE_MEMORY_MB, default 512,
clamped [64,16384]) and have the launcher append --max-old-space-size to the
child NODE_OPTIONS (a trailing flag wins, overriding the baked 256 without
clobbering other flags). Update the .env.example doc to reflect the 512 default.
Web-cookie providers (gemini-web, claude-web, claude-turnstile) need
Playwright/Chromium, which only ships in the runner-web image. The default
docker-compose.yml had no profile targeting runner-web, so 'docker compose up'
ran the base image and those providers failed with
'Executable doesn't exist at .../ms-playwright/chromium...'.
Add an 'omniroute-web' service (target runner-web, image omniroute:web,
profile 'web') and document it in the header. Validated with
'docker compose --profile web config'.
(A) For a Codex-only account, a bare gpt-5.5 Responses request was rerouted to
codex with the model hardcoded to gpt-5.5-medium (chatHelpers.ts). The Codex
executor reads a model-name suffix as an explicit modelEffort that, per #2331,
overrides the client's reasoning.effort — so a genuine reasoning.effort=xhigh
was silently demoted to medium. Keep the bare gpt-5.5 id (the connection
fallback still supplies the default effort); the executor precedence is
untouched, so #2331 stays intact.
(B) gpt-5.5-xhigh/-high/-low misrouted to the openai provider (only bare gpt-5.5
was codex-preferred), so codex-only users got 'No credentials for provider:
openai'. Add the suffixed variants to CODEX_PREFERRED_UNPREFIXED_MODELS so they
infer codex before the /^gpt-/ → openai fallback.
Closes#2877
handleChatCore declared `const settings = cachedSettings ?? await
getCachedSettings()` twice in the same function scope — the consolidated
"fetch once, reuse" const near the top, plus a duplicate added alongside the
per-key stream-default-mode feature. esbuild/tsx rejected the same-scope
redeclaration ("The symbol 'settings' has already been declared"), which made
every unit test that imports chatCore fail to transform and broke the
production build.
Remove the duplicate; reuse the earlier consolidated `settings`. Add an
import-guard regression test.
createSession() used rotator.random() despite its docstring saying 'next
available fingerprint'. Random draws collide in a small profile pool,
producing duplicate fingerprints (and duplicate sess-<fp>-<ms> ids) across
a warm pool — flaky tests and look-alike sessions. Switch to rotator.next()
so each pooled session gets a distinct fingerprint, as documented.
- duckduckgo-web.ts: Add poolConfig (2-5 sessions, 1s cooldown)
- providerRegistry.ts: Add poolConfig field to RegistryEntry type, configure for llm7
- default.ts: Read poolConfig from registry in constructor
DuckDuckGo Web now uses pool for VQD token rotation.
LLM7.io now uses pool with 2s cooldown for its 1 req/s rate limit.
Move pool integration from Pollinations-specific code into BaseExecutor
so any executor can opt in via a protected poolConfig property.
Changes:
- base.ts: Add poolConfig, getPool(), buildPoolHeaders() to BaseExecutor
- pollinations.ts: Remove static pool, use base class pattern
- session-pool-modular.test.ts: 36 tests for provider-agnostic pool behavior
Closes#2953
Codex can emit a function_call with an empty/missing call_id; the matching
function_call_output then becomes a role:'tool' message with no preceding
tool_call, which the upstream rejects: "Messages with role 'tool' must be a
response to a preceding message with 'tool_calls'". The orphan filter let it
slip through because its guard (`&& rec.tool_call_id`) short-circuited on the
empty id.
Two changes in openaiResponsesToOpenAIRequest:
- Skip function_call items with an empty call_id (mirrors the empty-name skip),
so no dangling assistant tool_call with an unmatched id is emitted.
- Drop ANY role:'tool' message whose tool_call_id (including empty/missing) has
no matching tool_call, instead of only those with a truthy id.
Closes#2893
opencode-zen serves the public, signup-free OpenCode Zen endpoint
(https://opencode.ai/zen/v1). With no API-key connection configured,
getProviderCredentials returned null, so the Playground/combos surfaced
"No credentials for provider: opencode-zen" when selecting an OpenCode free
model.
Fall back to synthetic no-auth credentials for opencode-zen when no usable
connection exists (a configured active key is still selected first; a
rate-limited/terminal key returns its own signal before this point). Other
api-key providers are unaffected.
Closes#2962
Fireworks Fire Pass (fpk_*) keys return 403 "Fire Pass API keys are not
authorized for this route." on /models while still serving chat. Two paths
wrongly penalized the key:
- validateOpenAILikeProvider returned "Invalid API key" for any 403 on the
models endpoint without trying the chat probe.
- checkFallbackError classified the 403 as AUTH_ERROR (retryable cooldown), and
even a generic fall-through hit the transient-cooldown default — marking the
connection unavailable.
Fix: validateOpenAILikeProvider now inspects the 403 body and falls through to
the chat probe for "not authorized for this route" responses (401 and generic
403 still fail fast). checkFallbackError short-circuits such route-restriction
403s to { shouldFallback: false, cooldownMs: 0 } so the connection is not cooled
down. Per CLAUDE.md, a generic api-key 403 should be recoverable unless terminal.
Closes#2929
The no-auth OpenCode provider has id "opencode" and alias "oc". The combo
builder built qualifiedModel from the provider id ("opencode/big-pickle"), but
parseModel("opencode/...") resolves to the opencode-zen api-key tier via a
manual ALIAS_TO_PROVIDER_ID override — not the no-auth "opencode" provider.
"oc/<model>" resolves correctly.
Rewrite qualifiedModel to the provider alias for no-auth providers (only when
the alias differs from the id), keeping providerId for getModelIsHidden. Aligned
the route test that previously asserted the buggy "opencode/" prefix.
Closes#2901
Codex Desktop injects an image_generation hosted tool into every Responses API
request (even text-only ones). The tool-type validator threw
unsupportedFeature() (400) for it, breaking every Codex Desktop request.
Mirror the tool_search handling: add IMAGE_GENERATION_TOOL_TYPES, allow it past
the validator guard, and drop it from the tools array before forwarding to Chat
Completions.
Closes#2950
The github provider has format:"openai" (baseUrl .../chat/completions) plus a
separate responsesBaseUrl (.../responses); a model only uses the Responses API
when it sets targetFormat:"openai-responses". GitHub Copilot's Responses API
does not serve Claude/Gemini models, so claude-opus-4.7, claude-opus-4-5-20251101,
gemini-3.1-pro-preview and gemini-3-flash-preview failed with [400]. The working
claude-opus-4.6 carries no targetFormat and uses chat/completions.
Drop targetFormat:"openai-responses" from those four Claude/Gemini entries so
they use the provider default. Native OpenAI gpt-* models (and oswe-vscode-prime)
keep the Responses API.
Closes#2911