A working Qoder PAT was reported as "expired". The validator probes the Cosy endpoint
(api1.qoder.sh) — which IS the correct PAT path (the executor falls back to it after the
expected 401 from api.qoder.com). The bug was the verdict: isCosyAppError (added by #2860)
marked ANY Cosy 500 with "success":false as an auth failure, including a generic
{..."msgCode":500,"message":"Internal Server Error"} server fault — contradicting the
older #1391 "5xx = valid bypass" rule.
Narrow it: a Cosy 500 only marks the PAT invalid when the body carries an EXPLICIT auth
signal (unauthorized/forbidden/expired/token invalid/...); a generic Internal Server Error
falls back to valid-bypass. #2860's protection for genuine auth rejections is preserved.
Regression test: tests/unit/qoder-cli.test.ts — the two pre-existing generic-500 cases now
assert valid:true (they encoded the #3247 bug) + a new explicit-auth-signal case asserts
valid:false. 13/13 green.
Follow-up to the #3269 private-webhook opt-in. With the opt-in on, the private-host
check was bypassed entirely, leaving cloud-metadata endpoints (169.254.169.254,
metadata.google.internal, 100.100.100.200, link-local 169.254.0.0/16) reachable — the
classic SSRF -> IAM-credential pivot — and the webhook test endpoint returned the
upstream body, making it a content-exfiltration primitive against internal services.
- outboundUrlGuard: add isCloudMetadataHost(); parseAndValidateWebhookUrl blocks those
hosts UNCONDITIONALLY, even when private targets are opted in.
- webhooks/[id]/test: redact responseBody for private targets (status + latency only).
Regression test: tests/unit/webhook-metadata-guard-3269.test.ts (RED before, GREEN after);
existing webhook SSRF/opt-in suites stay green (34/34).
Webhooks hardcoded parseAndValidatePublicUrl, which blocks any RFC1918/loopback host —
breaking self-hosted setups that legitimately point webhooks at internal services
(n8n, Home Assistant, a LAN box). Provider URLs already had an opt-in
(OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS); webhooks now reuse it.
- outboundUrlGuard: add parseAndValidateWebhookUrl — gates the private-host check on
arePrivateProviderUrlsAllowed() (default OFF); protocol + embedded-credential checks
stay unconditional.
- swap all webhook call sites (create/update/test/validate-url + dispatcher x2) to it.
Regression test: tests/unit/webhook-private-optin-3269.test.ts (RED before, GREEN after);
existing webhook SSRF/dispatcher suites stay green (33/33).
Audited every commit since v3.8.11 one-by-one. Added the missing v3.8.12
entries (features #3250/#3259/#3263/#3271, fixes #3248/#3249/#3261/#3256/#3274,
maintenance #3270), repointed the combo-rewrite and web-tools entries to their
actually-merged PRs (#3268, #3275) instead of the closed #3242/issue links, and
added the v3.8.12 Contributors hall. Also co-credited @ibanunmangun on the
v3.8.11 #3203 OAuth fix (independent first diagnosis via #3193).
A custom OpenAI-compatible image-edit provider received an empty `model`. In production
`globalThis.fetch` is patched with node_modules/undici's fetch, whose `FormData` class
differs from `globalThis.FormData`; passing a native FormData made undici serialize it as
the string "[object FormData]" (text/plain), dropping every field including `model`.
handleOpenAIImageEdit now assembles the multipart body as a Buffer with an explicit
boundary + Content-Type, which every fetch impl accepts verbatim.
Regression test: tests/unit/image-edits-multipart-3273.test.ts reproduces the exact prod
condition (routes through undici's fetch) — RED before (upstream got text/plain
[object FormData]), GREEN after. Existing image suites stay green (50/50).
Regression of #764. Claude Code → Groq (llama-3.3-70b-versatile) returned HTTP 400
because the model was treated as reasoning-capable: supportsReasoning() defaulted to true,
so applyThinkingBudget did not strip reasoning params, and the claude→openai translator
forwarded reasoning_effort (and re-injected it from output_config.effort) — which Groq
rejects on non-reasoning models.
- providerRegistry: mark llama-3.3-70b-versatile + llama-4-scout supportsReasoning:false
(gpt-oss / qwen3-32b keep reasoning — they accept reasoning_effort).
- stripThinkingConfig: also strip output_config.effort so the translator can't re-inject
reasoning_effort downstream.
Regression test: tests/unit/thinking-budget-groq-3258.test.ts (RED before, GREEN after);
existing thinking-budget suites stay green (45/45).
ds-web/deepseek-v4-pro emits tool calls wrapped as
<tool_call name="skill">{"name":"customize-opencode"}</tool_call> instead of the
canonical <tool>{json}</tool>. webTools.ts only matched <tool>...</tool>, so the block
was silently dropped (and when arguments were present, the surrounding tag leaked into
content). Add TOOL_CALL_TAG_RE to capture the JSON body — the real tool name comes from
the body, never the tag's name= attribute — and extend the early-exit + range stripping.
Regression test: tests/unit/web-tools-translation-3260.test.ts (RED before, GREEN after).
Existing web-tools suites stay green (26/26).
Bump 3.8.11 → 3.8.12 across package.json, lockfile, electron/, open-sse/, and
docs/reference/openapi.yaml; add the [3.8.12] cycle placeholder to the root
CHANGELOG and the 41 i18n mirrors. Integration branch for the v3.8.12 cycle —
fixes/features land here via per-issue PRs and it merges to main at release time.
CodeQL flagged 7 high alerts in code the cycle touched (the large release-PR diff
re-surfaces them). Resolved at the source — no dismissals:
- fix(images): resolveImageBaseUrl trimmed trailing slashes with `/\/+$/`, a
polynomial-ReDoS pattern (js/polynomial-redos) on the configured node base URL.
Replace it with a non-backtracking endsWith/slice loop.
- test(oauth): pin the Anthropic OAuth host with exact-equality asserts and a
parsed-hostname negative check instead of substring `.includes()`
(js/incomplete-url-substring-sanitization). The exact-equality assertions were
already present, so coverage is unchanged.
- test(images): drop the redundant `!includes("generativelanguage.googleapis.com")`
assert — the exact-equality assert on the resolved URL already guarantees it.
Clear the two release-gate failures the CI Lint+Build jobs surfaced (release-branch
drift — PR merges bypassed pre-push):
- fix(api): /v1/images/edits parsed request.json() without a Zod guard
(route-validation t06 / hard rule #7). Add ImageEditJsonSchema.safeParse so a
malformed body (non-object / wrong types) is rejected with 400 instead of
silently parsed; valid JSON/data-URL bodies behave exactly as before. (#3214, #3215)
- fix(dashboard): remove a duplicate handleToggleProxyEnabled /
handleTogglePerKeyProxyEnabled / handleDistributeProxies block in
providers/[id]/page.tsx — a bad merge of the proxy PRs declared all three twice,
breaking the webpack build ("Identifier already declared"). The removed copy was
byte-identical to the kept one. (#3170, #3171, #3172)
Finalize the 3.8.11 cycle CHANGELOG and clear the failures the full test:unit
gate surfaced (release-branch drift — PR merges bypassed pre-push):
- CHANGELOG: date the [3.8.11] section (2026-06-05) + repo-housekeeping roll-up
- docs(env): document THEOLDLLM_NAV_TIMEOUT_MS in .env.example + ENVIRONMENT.md
(env-doc-sync gate; #3217 added the var without docs)
- test(nvidia): exercise the #3226 bypass-fetch path via a local HTTP server and
hoist the validator import (patching globalThis.fetch no longer intercepts the
un-patched native fetch captured by proxyFetch)
- test(i18n): import the shipped normalizeComplianceEventTypes helper (#3185)
- test(model-caps): save synced metadata under the canonical gemini-3.1-pro key
now that #3229 aliases gemini-3.1-pro-high/-low to it
- test(web-session): expect the grok-web "sso + sso-rw" credential hint (#3180)
- test(synced-models): isolate DATA_DIR so the #3199 hidden-override stops
bleeding into the shared DB and breaking the re-run precondition
agy's gemini-3.1-pro-high/-low had no alias, so resolveAntigravityModelId sent the
speculative -high/-low suffix verbatim to upstream, which rejects it (400) for
gemini-3.x. Worse, the non-stream executor branch fed the 4xx response into the SSE
collector, returning a synthetic empty {object:chat.completion} envelope that masked
the error. Alias both to gemini-3.1-pro, and surface real upstream errors via
buildErrorBody for non-ok non-stream responses. + unit tests.
The Codex CLI WS->HTTP fallback rewrite (resolveResponsesApiModel) prefixes a
bare model id with codex/ whenever codex/<id> resolves to codex. Codex accepts
arbitrary model strings, so a combo name with no slash (e.g. n8n-text,
paid-premium) was rewritten to codex/<combo> and sent to Codex instead of being
resolved as a combo — regressing combos via /v1/responses in v3.8.9+. Skip the
rewrite when the bare id is a combo (getComboByName). + unit test.
- grok-web: the credential hint named only "sso" while Grok needs both "sso"
and "sso-rw"; users pasted just sso and hit anti-bot 403s. Name both
(credentialName + placeholder). The underlying Cloudflare anti-bot 403 is
upstream and tracked separately on #3180.
- vertex: the Service Account JSON placeholder was an untranslated stub literal
("Vertex Service Account Placeholder") in 40 locales, making the field look
broken even though SA-JSON auth is fully supported. Replace with real
instructional text (zh localized; pt-BR already translated).
Guard test pins both hints.
The NVIDIA key-validation chat probe used models[0] (z-ai/glm-5.1), which
requires the 'Public API Endpoints' account permission and has DEGRADED
windows. Accounts lacking that permission see the probe hang until the
validation timeout, surfacing as a misleading 'Upstream Error' on a valid key.
Probe the universally-available meta/llama-3.1-8b-instruct instead, still
overridable via providerSpecificData.validationModelId. + unit test.
getProviderConfig falls back to { name: providerId } for unknown ids, so the
label precedence config.name || p.name let the raw internal UUID of a custom
provider shadow the friendly name HomePageClient already resolved into p.name.
Extract resolveTopologyNodeLabel (entry name first) + unit test.
Image routes now resolve a requested model the same way across /v1/images/generations
and /v1/images/edits, via a shared resolver: built-in id -> custom provider prefix ->
bare combo/alias name (e.g. "image" -> its single image target). Previously a bare
combo name fell through to "Invalid image model".
/v1/images/edits gains two capabilities for custom OpenAI-compatible providers:
- multipart edit forwarding to the node's {base_url}/images/edits (was hard-rejected
unless chatgpt-web);
- JSON/data-URL edit input (images:[{image_url:"data:..."}]), converted to the same
fields the multipart reader produces (was "Invalid multipart body").
The chatgpt-web conversation-continuation edit flow is unchanged.
The db-backups import route statically imported better-sqlite3, which is
stripped from the Next standalone server's node_modules in the packaged
Electron app. Loading the route then crashed with "Cannot find module
'better-sqlite3'" on the Windows installer, even though node:sqlite was
available. Route the upload integrity-check through openDatabaseAsync
(better-sqlite3 -> node:sqlite -> sql.js), matching every other DB path.
Adds a guard test so no API route can reintroduce a direct native import.
Follow-up to #3204: deleting a synced (fetched) model removed it from the
synced set, but the DELETE route didn't mark it hidden and the re-import path
didn't skip hidden ids, so the next auto-fetch re-added it. Now the route
marks the id hidden on delete and replaceSyncedAvailableModelsForConnection
filters hidden ids, so the deletion sticks.
Co-authored-by: tjengbudi <tjengbudi@users.noreply.github.com>
Residual of #3136: a local connection with an empty baseUrl still resolved
to this.config.baseUrl (OpenAI). Fall back to the provider's localDefault
(127.0.0.1:8080/v1) before the OpenAI default for the local-provider group.
Co-authored-by: tjengbudi <tjengbudi@users.noreply.github.com>