Skip to content

fix(kilocode): downgrade json_schema response_format for DeepSeek V4 Flash (400 regression) - #10454

Closed
benzntech wants to merge 2189 commits into
diegosouzapw:mainfrom
benzntech:fix/kilocode-deepseek-json-schema-fallback
Closed

fix(kilocode): downgrade json_schema response_format for DeepSeek V4 Flash (400 regression)#10454
benzntech wants to merge 2189 commits into
diegosouzapw:mainfrom
benzntech:fix/kilocode-deepseek-json-schema-fallback

Conversation

@benzntech

Copy link
Copy Markdown
Contributor

Problem

kilocode/deepseek/deepseek-v4-flash rejects response_format: {type: "json_schema"} with HTTP 400 Invalid input: response_format (verified live 2026-08-15 via the Hindsight fact-extraction path — upstream_details.param: response_format). Any client using Structured Output against kilocode's DeepSeek fails.

The default executor's applyJsonSchemaFallback (which downgrades json_schema → json_object with the schema injected into the system prompt) is gated on provider.startsWith("openai-compatible-") only — so kilocode forwarded the unsupported format raw. Same bug class as the opencode fix in #9992, which covered the opencode/opencode-zen DeepSeek route but not kilocode's.

Fix

Extend applyJsonSchemaFallback's gate to include kilocode. json_schema is downgraded to json_object, with the JSON schema injected into the system prompt — identical to the existing openai-compatible-* behavior. Native-Structured-Output providers (openai etc.) keep the native json_schema path untouched.

Regression test

tests/unit/executor-default-base.test.ts: new case asserts a kilocode request with json_schema response_format is downgraded to json_object + schema-injected system message, and the original body is not mutated. Verified to fail without the fix (sabotage run: 1 fail) and pass with it. All 48 executor-default-base tests pass.

diegosouzapw and others added 30 commits August 9, 2026 18:55
…-three-defects

fix(logging): make stream-chunk capture and request-shape logging opt-in
…r-guide-ui

feat(providers): add Cookie Editor fast-path to web session credential guide
…apw#9964)

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
…windows-ci-coverage

feat(ci): add windows-latest leg to test-bun-sqlite job (diegosouzapw#8468)
…gpt-endpoint-surface

feat: expose full NanoGPT endpoint surface (diegosouzapw#9322)
…ni-schema-recursive-type-empty-choices

feat(gemini): recursive schema type:object + empty choices interceptor (diegosouzapw#9268)
…3b-9424

# Conflicts:
#	config/quality/file-size-baseline.json
…iegosouzapw#9917)

* fix(i18n): re-escape CC discovery-alias angle brackets for next-intl

Restore diegosouzapw#8747 HTML-entity escaping for claude/<provider>/<model> in the
three CC discovery-alias message keys so next-intl stops logging
INVALID_MESSAGE: UNCLOSED_TAG on provider detail pages after the bulk
entity-unescape regression.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(i18n): align conflict context with release

* fix(i18n): cover localized CC alias placeholders

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
…r-a-capability-filter

feat(core): add Layer A capability filter at router (diegosouzapw#5696)
…lector, stats, test button) (diegosouzapw#9782)

* feat(i18n): modality bridge page strings (en + synced locales)

* feat(dashboard): ModalityBridgeVisionTab + stats row + test button

* feat(dashboard): Modality Bridge settings page with vision/audio/video tabs + sidebar entry

* feat(dashboard): relocate vision bridge card to link + media-providers shortcuts

* docs(guardrails): document Modality Bridge dashboard

* chore: preserve upstream formatting after base merge

* fix(modality-bridge): satisfy i18n quality gates

* fix(i18n): preserve canonical Chinese glossary terms

* fix(modality-bridge): clear dashboard quality regressions

* fix(settings): use catalog-only modality labels

* fix(i18n): isolate modality bridge availability copy

* chore(i18n): prepare conflict-free Modality Bridge base sync

* docs(modality-bridge): align migration note with dead-code decision

* fix(i18n): sync capability filter locales after release merge

* fix(i18n): restore canonical Traditional Chinese glossary

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…iegosouzapw#9922)

Add examples/quickstart/ with minimal copy-paste scripts that let new
users get a response from a local OmniRoute server in under a minute,
without needing to read the full docs first.

Files added:
- examples/quickstart/python_requests.py  (requests library)
- examples/quickstart/nodejs_axios.js     (axios)
- examples/quickstart/curl_terminal.sh    (bash one-liner)
- examples/quickstart/php_curl.php        (cURL extension)
- examples/quickstart/README.md           (table + key-settings cheatsheet)

README.md: add one sub-line pointer to examples/quickstart/ below the
existing zero-config curl snippet, matching the surrounding <sub> style.
…iegosouzapw#9924)

* fix(memory): allow OMNIROUTE_STRICT_SYSTEM_PROVIDERS to extend the system-first provider list

PROVIDERS_SYSTEM_MUST_BE_FIRST (added in diegosouzapw#6225 for diegosouzapw#6135) gates both the
memory-injection placement fix and the diegosouzapw#7293 hoistLeadingSystemMessage
translator fix, but was hardcoded to xiaomi-mimo/mimo only. Self-hosted
deployments routing other strict backends (e.g. a custom OpenAI-compatible
connection in front of a self-hosted Qwen3.5+/3.6 model, whose chat template
rejects any non-leading system message the same way) had no way to opt in
without forking and rebuilding the image.

Adds OMNIROUTE_STRICT_SYSTEM_PROVIDERS (comma-separated, case-insensitive
provider ids) to extend the built-in set at read time, mirroring the
injectable-env pattern already used in src/lib/memory/typedDecay.ts. No
behavior change for anyone who doesn't set it.

* chore: fix changelog fragment PR number
…egosouzapw#9959)

process.uptime() returns a number, but the handler ran it through a
string-only toString() helper that fell back to "unknown" for anything
that wasn't already a string -- so every real uptime value was
discarded, 100% reproducibly.

Also stop masking upstream fetch failures as fake healthy defaults:
when /api/monitoring/health, /api/resilience, or /api/rate-limits
can't be reached, the tool now reports which source failed (via a new
optional `degraded` field) instead of returning zeros/empty arrays
indistinguishable from genuine "no data".

Regression coverage dispatches through the real MCP handler (client.callTool)
rather than asserting on the mock directly, since the prior mock-only
tests could never have caught either bug.
…mn (diegosouzapw#9963)

Both tables (002_mcp_a2a_tables.sql) store their row timestamp in
created_at; the cleanup queries used WHERE timestamp < ? which does not
exist, so every boot-time cleanup logged:
  Error cleaning mcp_tool_audit: SqliteError: no such column: timestamp
  Error cleaning a2a_task_events: SqliteError: no such column: timestamp
and retention pruning for these two tables never ran. Fix the DELETE
columns and align the log labels/doc comments with the real table names.

Adds source-level invariant tests (cleanup-column-fix.test.mjs) asserting
the created_at column for both tables.
amartinawi and others added 24 commits August 13, 2026 04:42
…w#10101)

loadEnvFile() took everything after the first '=', so 'KEY=value  # note' stored
the comment text as part of the value. The shipped .env/.env.example do exactly
that for QUOTA_STORE_DRIVER, so every install ran with
QUOTA_STORE_DRIVER='sqlite              # sqlite | redis'.

Consumers compare with '===' (storeFactory.ts), so a user following the
annotation in .env.example and writing 'QUOTA_STORE_DRIVER=redis  # ...' got
driver !== 'redis', fell through to SQLite, and saw no warning — the existing
'no Redis URL configured' warning is inside the redis branch and never fires.

parseEnvValue() adopts dotenv semantics: quoted values verbatim (a '#' inside
quotes is data), unquoted values cut at the first whitespace-preceded '#', so
'pass#word' survives. .env.example moves the annotation to its own line.

Closes diegosouzapw#10100
…iegosouzapw#10086)

Probed live: /chat/completions answers HTTP 200 with no Authorization
header (kilo-auto/free routed to stepfun/step-3.7-flash). A real key
still raises limits, so this matches the ovhcloud/pollinations pattern
of authType: "optional" rather than "apikey".

Fixes diegosouzapw#10068
…pw#10090)

checkNativeBinary only probed the node-gyp layout
(build/Release/better_sqlite3.node), which exists only when better-sqlite3 is
compiled locally. Installs that resolve a prebuilt binary — the normal case for
`npm i -g omniroute` — ship prebuilds/<platform>-<arch>.node instead, so the
check never found a binary and warned "better-sqlite3 native binary was not
found" on every such install, next to real warnings.

Probe both layouts and report both in the failure details. prebuiltBinaryName()
mirrors the prebuild-install lookup, including the linuxmusl- prefix for
musl-based Linux.

Closes diegosouzapw#10083
…shape (diegosouzapw#10091)

GET /api/openapi/spec answers with a compact catalog
({ info, servers, tags, endpoints[], schemas }) rather than an OpenAPI document,
while dist/docs/openapi.yaml is a real spec. The CLI only read spec.paths, so
against a live server 'openapi endpoints' and 'openapi paths' printed nothing
and 'openapi validate' reported 'missing openapi/swagger version field' — with
318 endpoints sitting in spec.endpoints.

Normalize both shapes through extractEndpoints()/extractPaths() and let
validateBasic() accept a catalog that carries endpoints[] instead of a version
field. Path Item members that are not operations (parameters, summary,
description, servers, $ref) are no longer emitted as fake operations.

Closes diegosouzapw#10082
…ts (diegosouzapw#10092)

Three commands turned a transport failure into something that reads as real
state:

- `keys add` aborted on any 4xx. `/api/v1/providers/keys` is not mounted on
  the shipped server, so a 404 stranded the user with "HTTP 404" while the
  SQLite fallback directly below it — which works — was unreachable whenever the
  server was up. New isRouteUnavailableStatus() (404/405/501) lets the caller
  fall through; genuine client errors (400/401/403/409/422/429) stay fatal.

- `providers test-all` reported every OAuth connection as FAILED because
  getProviderApiKey() throws for non-apikey connections by design — and
  persisted that verdict to provider_connections.test_status, marking healthy
  OAuth providers broken. Those connections are now skipped. An "unsupported"
  probe result (no recipe in PROVIDER_TEST_CONFIGS) is likewise a CLI gap, not
  a provider failure, so it no longer overwrites a good test_status.

- `combo list` printed "No combos configured" when /api/combos returned
  non-2xx, which is indistinguishable from genuine emptiness. It now reports the
  status and exits non-zero.

Refs diegosouzapw#10081
…ack (diegosouzapw#10097)

loadAvailableProviders() always returned COMMON_PROVIDERS, so 'omniroute keys
add <provider>' rejected ~290 of the ~296 catalog providers with 'Unknown
provider' and 'providers available' under-reported the catalog by ~98%. Two
independent causes, either sufficient on its own:

1. extractProviderBlocks() required 'typescript' at runtime, but it is only a
   devDependency — absent from every published/global install. The require
   failure was swallowed and the parse returned [].

2. The parser read src/shared/constants/providers.ts, which after the god-file
   decomposition contains only re-exports plus an empty 'FREE_PROVIDERS = {}'.
   Even with typescript present it yielded zero entries.

Replace the AST parse with a dependency-free, string/comment-aware brace walk
(these files are pure data literals) and walk src/shared/constants/providers/**
instead of the barrel. An explicit catalogPath / OMNIROUTE_PROVIDER_CATALOG_PATH
still wins, and the COMMON_PROVIDERS fallback still applies when no catalog is
present.

The walk is also hardened against an unbalanced literal (diegosouzapw#10093): it recovers
the entries before the damage and terminates, instead of looping forever on a
reset regex lastIndex.

Closes diegosouzapw#10080
…ink + discount-first banner copy (diegosouzapw#10240)

Moonshot approved a 15% extra-credits offer for new users' first top-up,
attached to a dedicated tracked link issued for OmniRoute (valid through
2026-09-30). The dashboard banner and the three README API platform
placements now use that link, and the banner description leads with the
discount in all 43 locales, keeping the commitment made when the offer
was requested. The README CTA gains the 15% mention. A code comment
marks the strings to revisit after 2026-09-30 if the offer is not
renewed.

Co-authored-by: backryun <bakryun0718@proton.me>
…nzntech)

- buildSourceUpdateScript: pnpm install/build, named autoupdate/<version> branch
  (patchable), pm2 restart retained, no auto-cherry-pick (Hermes applies the
  fix patch as a manual step)
- /api/system/version source flow: same pnpm + named-branch treatment
- CLI update --apply/--dry-run: source-install aware (git+pnpm, never
  npm install -g which would clobber the link)
- bin/omniroute.mjs notifier: source-aware update hint
- pnpm-workspace.yaml: fix allowBuilds placeholders (ERR_PNPM_IGNORED_BUILDS)
- tests: updated source-script assertions to pnpm/named-branch
- DOCUMENTATION.md: maintenance manual (update procedure, patch set, rollback,
  stop-patching switch)
… route

The /api/settings/quota/state route imports clearProviderQuotaState(connectionId,
model) but providerQuotaState.ts only defined clearProviderQuota(connectionId) —
a build blocker and a bug carried in open PR diegosouzapw#10126. Adds the model-scoped
variant on the (connection_id, model) keyed table.
…S (env-doc-sync gate)

The source-aware CLI update (update.mjs) reads AUTO_UPDATE_GIT_REMOTE via
process.env, which the check-env-doc-sync extractor now catches. Documented
both auto-update knobs in .env.example + ENVIRONMENT.md.
…base

- combo.ts: import getCachedProviderConnectionById via @/lib/localDb alias
  (the PR's ../../src/lib/localDb.js path fails to resolve in Turbopack
  app-route builds; every sibling uses the alias)
- combo.ts: drop duplicated max-concurrency gate block (rebase merge artifact
  returning null instead of stopProtectedPriorityTarget)
- eslint-suppressions.json: allow the barrel import in combo.ts (matches
  sibling concurrencyCaps/quotaExhaustionCutoff suppressions)
- pnpm-workspace.yaml: allow opencode-ai build script (release base dep)
…ly dir

A linked/source install's standalone dist/ ships its own package.json, so the
walk-up stopped at dist/ and the git-based auto-update reported 'Not a git
repository'. Now prefers a .git checkout anywhere above, falling back to the
first valid package.json dir (preserving the release base's synthetic
standalone-package.json guard).
- package.json: declare playwright-core + remark-gfm as direct deps (npm's
  hoisting had masked them; pnpm's isolated layout cannot resolve them from
  transitive-only positions) — both are directly imported in source
- prepublish.ts: use runBuildTool() for the ChatGPT-Web (Codex) MCP bridge
  bundle instead of raw npx esbuild, which fails with status 127 under
  pnpm's isolated node_modules
- assembleStandalone.mjs: remove pre-existing symlink at destination before
  recursive copy (ERR_FS_CP_DIR_TO_NON_DIR with pnpm's symlinked layout)
- pnpm-workspace.yaml: allow opencode-ai build script
- pnpm-lock.yaml: regenerated for pnpm (includes omniroute link override)
…rojectRoot

Standalone chunks replace the project root with the /ROOT placeholder
(outputFileTracingRoot), so the compiled default startDir (/ROOT/src/lib/system)
does not exist at runtime — the git-based auto-update reported 'Not a git
repository'. When the start dir is the unreachable /ROOT placeholder, walk up
from process.cwd() instead (server.js chdirs to the real standalone dir).
…encies

pnpm 11 reads onlyBuiltDependencies (array), not just the allowBuilds map —
both packages' build scripts were silently ignored, leaving the Playwright
browser download and bun binary setup unrun.
The OpenAPI spec and the CLI (omniroute providers rotate, generated
api-commands) both use PATCH /api/providers/[id], but the route only
implemented PUT — PATCH requests returned 405 and key rotation via the
CLI silently failed while reporting success (the DB-write fallback only
catches thrown exceptions, not non-OK HTTP responses).

Add a PATCH handler delegating to the PUT handler: both apply the same
partial-update schema, so the semantics are identical.

Regression test proves the PATCH export exists and delegates into the
shared auth path; verified to fail without the fix.
…gression)

kilocode's DeepSeek V4 Flash rejects ANY response_format — both
json_schema AND json_object 400 with 'Invalid input: response_format'
(verified live 2026-08-15 via the Hindsight fact-extraction path on
kilocode/deepseek/deepseek-v4-flash). The default executor's
applyJsonSchemaFallback only covered openai-compatible-* providers and
only downgraded json_schema -> json_object, so kilocode forwarded the
unsupported format raw. Same bug class as the opencode fix diegosouzapw#9992.

For kilocode: strip response_format entirely and inject the schema (or a
plain 'valid JSON only' instruction for json_object) into the system
prompt. openai-compatible-* keeps the existing json_schema downgrade and
json_object passthrough (they accept both).

Regression tests: kilocode json_schema is stripped + schema-injected;
kilocode json_object is stripped + JSON-only instruction; both verified
to fail without the fix (sabotage: 2 fail). All 49 executor-default-base
tests pass.
@benzntech
benzntech force-pushed the fix/kilocode-deepseek-json-schema-fallback branch from 4a9af38 to a0b2000 Compare August 15, 2026 08:02
@benzntech

Copy link
Copy Markdown
Contributor Author

Updated the fix after live verification: kilocode's DeepSeek rejects BOTH json_schema AND json_object response_format (both 400 with param: response_format — verified by direct gateway tests). So the downgrade-to-json_object approach (which fixes openai-compatible-*) is insufficient for kilocode.

Refined fix (force-pushed): for kilocode, strip response_format entirely and inject the schema (or a plain 'valid JSON only' instruction for json_object requests) into the system prompt. openai-compatible-* keeps the existing json_schema→json_object downgrade and json_object passthrough (they accept both natively).

Regression tests now cover: kilocode json_schema → stripped + schema-injected system message; kilocode json_object → stripped + JSON-only instruction. Sabotage-verified (2 fail without the fix). All 49 executor-default-base tests pass.

@benzntech

Copy link
Copy Markdown
Contributor Author

Superseded by #10458 — this branch accidentally carried the custom-fork history (100 commits). #10458 is the clean single-commit version of the same fix (2 files, +92/−11, sabotage-verified).

@benzntech benzntech closed this Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.