mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 09:02:11 +03:00
17e4ddfc77c9431ad154ed362ec1145ec00a1e96
109 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
04dba0460e |
fix(responses-continuation): recover a real id/output for passthrough and translate-mode replies (#11434)
Retargetado para release/v3.8.51 (release/v3.8.50 está congelada — freeze issue #11439). Validado em lote combinado (batch-0824h2, junto de #11435/#11436/#11437) contra o tip de release/v3.8.51: typecheck:core limpo, gates estáticos OK, 127/127 testes focados passando. Investigação sólida com repro real via container isolado, três causas independentes identificadas e corrigidas com testes de regressão dedicados para cada uma. Obrigado pela contribuição! |
||
|
|
a3c3117254 |
fix(translator): preserve omitted subagent session IDs (#11297)
Validated on a 3-PR combined board: openai-responses-opencode-subagent-sessionid within the board's 20/20, typecheck:core + check:open-sse-typecheck clean, gates within baseline. Uses null as the omission sentinel for OpenCode's optional default-less subagent.sessionID under Responses/Codex strict mode (which required every declared property) — models can now omit the field to spawn a new child instead of inventing filler IDs; the null is stripped before OpenCode sees the call, real child IDs pass through unchanged. Thank you @ofonseca-pyming! |
||
|
|
1dd0173468 |
fix(sse): assign a stable index/id to parallel function_call items in Responses->Chat translation (#11144)
Validated on the combined batch board over release/v3.8.50 tip
|
||
|
|
00aba8ef16 |
fix(sse): include prompt cache usage fields on message_stop fallback path (#10545)
Mirrors the existing prompt_tokens_details cache-field mapping from the message_delta finish path into the message_stop fallback, so OpenAI-compatible clients see cache_read/cache_creation counters when the finish signal arrives without usage on the same event. Fixes #10535. Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts, 2 files): - 16/16 focused tests pass (translator-resp-claude-to-openai.test.ts), including the new regression test for this exact fallback path. - check-file-size, check-changelog-integrity: OK. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. Co-authored-by: NahuSaruf <NahuSaruf@users.noreply.github.com> |
||
|
|
769ab62fa3 |
fix(reasoning): preserve compatible response state (#10574)
Preserves authentic plaintext reasoning continuations across Chat Completions and Responses (streaming + non-streaming), applying one target-aware reasoning transport policy before protocol translation. Fixes #10550. Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts, 39 files): - 446/446 focused node:test tests pass (chat-route-coverage, chatcore-translation-paths, combo-attempt-body-isolation-7847, combo-config, executor-codex, kimi-coding-translator, moonshot-k3, reasoning-cache, response-sanitizer, responses-handler, responses-translation-fixes, strip-reasoning-blobs-agentic-context-1599, translator-openai-responses-req). - 12/12 vitest tests pass (edit-connection-modal-free-models.test.tsx). - check-changelog-integrity: OK. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. - file-size: chatHelpers.ts crossed the frozen cap by +2 lines (irreducible reasoningTransportFallback option threading) — rebaselined 1017->1019 with justification, pushed to the PR branch (fix-in-place), re-validated after a base-drift re-merge against the latest release tip. Co-authored-by: jackjinke <jackjinke@users.noreply.github.com> |
||
|
|
2cd14b1696 |
fix(providers): rename Freepik slug to Magnific and validate Magnific API keys (#10594)
Canonical provider id renamed freepik → magnific (Magnific Mystic official API), with a permanent redirect + runtime alias so old freepik/<model> traffic and /dashboard/providers/freepik URLs keep working. Existing provider=freepik connection rows are rewritten to magnific by migration 160. Closes #10604. Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts, 138 files): - Focused suite: 54/54 tests pass (magnific-image-handler, provider-validation-image-only, provider-alias-uniqueness, redirects-cli-renames). - check-file-size, check-changelog-integrity: OK. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. Co-authored-by: RaviTharuma <RaviTharuma@users.noreply.github.com> |
||
|
|
548316a2c4 |
fix(translator): Normalize tool call names from lowercase to PascalCase when translating upstream responses to Claude Messages API format (#10392)
* fix(translator): Normalize tool call names from lowercase to PascalCase (#1) * Fix: Map lowercase tool names from Antigravity (Gemini format) to Claude Code expected PascalCase * Fix: toolNameMap in fun restoreClaudePassthroughToolUseName * fix(translator): Normalize tool call names from lowercase to PascalCase when translating upstream responses (OpenAI, Gemini, Antigravity) to Claude Messages API format This resolves `Error: No such tool available: read`/`bash`/`write` errors when using Claude Code CLI with third-party providers that emit lowercase tool names. The fix adds case-insensitive tool name lookups in `openai-to-claude.ts`, `gemini-to-claude.ts`, and related translators, ensuring tool names like `read`/`bash` are mapped to `Read`/`Bash` before being sent to Claude Code. Includes unit tests and comprehensive changelog notes ([#10250](https://github.com/diegosouzapw/OmniRoute/pull/10250)) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * fix(translator): Parse <tool_call> JSON and TOOL_CALL text formats fr… (#2) * fix(translator): Parse <tool_call> JSON and TOOL_CALL text formats from model output Some models (DeepSeek, Qwen) emit tool calls as text instead of proper tool_calls JSON: either <tool_call>{...}</tool_call> or TOOL_CALL Name: {...}. Extend extractXmlInvokeBlocks to handle all 3 formats in a single scan pass, picking whichever pattern appears first. Includes unit tests for all formats. * fix(translator): Parse text-format tool calls in gemini-to-claude translator Extend the Gemini->Claude translator to detect <invoke>, <tool_call> JSON, and TOOL_CALL text formats emitted inline in text parts (Antigravity/Gemini models), converting them to proper tool_use content blocks instead of leaking raw text to Claude Code. * docs(changelog): Add changelog entry for text tool call parsing fix * fix(translator): consolidate tool name casing normalization and restore thought-signature persistence (#3) * fix(translator): sanitize tool_use.id and tool_result.tool_use_id to match Anthropic schema (#4) Ensure tool IDs from OpenAI-compatible upstreams (which may contain dots, colons, or special characters) are sanitized to ^[a-zA-Z0-9_-]+$ in response translators and passthrough requests before reaching Claude endpoints. * fix(responses): preserve native tools for openai-compatible Responses targets (#5) A Responses-shaped request to a custom openai-compatible connection whose outbound protocol is Responses took a Responses -> Chat -> Responses round trip, so Codex custom tools lost their grammar (`exec`), namespace groups were flattened (`collaboration`), and tool invocations failed upstream. Gate a native Responses passthrough on the connection's configured protocol (`apiType: "responses"` / `_omnirouteForceResponsesUpstream`) so the original tool definitions reach a Responses-capable upstream unchanged. Chat-only connections keep the existing downgrade. Closes #10374 * fix(translator): add support for 'applypatch' tool name in tool call checks * test(translator): add unit test for apply_patch and applypatch tool name remapping * fix(translator): remove no-explicit-any lint errors in tool-use-id-sanitization test Type the openaiToClaudeResponse/translateNonStreamingResponse return values with narrow local shapes instead of `any`, satisfying the repo's no-explicit-any = error rule for tests/. No behavior change — the same 3 assertions still pass. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * test: update 9568 casing regression to match #10392's consolidated fix restoreClaudeToolName's static casing map now normalizes known lowercase tool names to canonical PascalCase unconditionally on the gemini-to-claude and openai-to-claude Claude Messages API paths (not gated behind toolNameMap), superseding the earlier per-map-only fix that the original #9568 regression test locked in as "expected" (it was previously labeled a known bug case). The gemini-to-openai passthrough path is unaffected by #10392 and keeps its original pass-through assertion. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
0a72988bde |
fix(responses-api): explicit function-tool declaration must win over apply_patch-is-custom fallback (#10041)
open-sse/translator/response/openai-responses.ts's isCustomTool check
unconditionally treats any tool named "apply_patch" as a Codex-style
custom tool: `toolName === "apply_patch" || state.customToolNames?.has?.(toolName)`.
This overrides a client's own explicit declaration whenever it registers
apply_patch as a plain `type:"function"` tool (with its own JSON-schema
parameters) instead of `type:"custom"`.
Live incident: OpenClaw (combo "default" -> opencode-zen/big-pickle)
declared apply_patch as `type:"function"` with `{input:string}`
parameters. The model correctly produced valid JSON matching that
schema (`{"input":"*** Begin Patch..."}`), but OmniRoute unwrapped it
into a custom_tool_call with raw-text `input` instead of the
function_call/`arguments` shape the client actually registered.
OpenClaw's own dispatcher only implements function_call handling for a
name it declared as type:"function", so it silently never recognized
the tool call at all -- no error, no execution, no follow-up request
ever carrying a result back to the model.
Traced the exact live code path (chatCore.ts -> createSSEStream
translate mode -> translator/index.ts's hub-and-spoke openai ->
openai-responses conversion) to confirm this file -- not
transformer/responsesTransformer.ts -- is what handles combo-routed
streaming for this client/provider format pair.
PR #7905 ("Restore Responses API custom tool calls") already states
this exact precedence should hold ("...while preserving explicit
function-tool precedence") but its unconditional `toolName ===
"apply_patch"` OR never actually implemented that carve-out for
apply_patch specifically -- this fixes the gap between that PR's
stated intent and its actual behavior.
Fix: state.toolSchemas (populated from body.tools by
extractToolSchemaMap(), already threaded through stream.ts's translate
state for a different purpose -- #6951's stripEmptyOptionalToolArgs)
only contains an entry for a tool name when the client's request
declared it with a `parameters` JSON schema, i.e. as type:"function".
Gate the apply_patch fallback on NOT finding it there: apply_patch
still defaults to custom (native Codex CLI convention -- the model
emits it without the client ever declaring it as a tool) unless the
client explicitly registered it as a function tool, in which case that
explicit declaration wins.
Test plan:
- TDD: two new regression tests in
tests/unit/translator-openai-responses-custom-tool-1007.test.ts --
"...with tool defined" (function_call, arguments stay raw JSON) and
"...without tool defined" (unchanged custom_tool_call fallback,
mirroring the existing #1007 coverage). The "with" test is confirmed
failing against the pre-fix code, passing after; the "without" test
passed before and after (regression guard for the existing fallback
behavior).
- Full related suite (translator-openai-responses-custom-tool-1007,
responses-handler, responses-active-stream-custom-tool,
translator-resp-openai-responses,
translator-resp-openai-responses-namespace-identity,
translator-openai-responses-image-output-8459,
responses-transformer) -- 64/64 passing, no regressions to PR #7905's
own custom-tool coverage.
- npx tsc --noEmit -- clean (pre-existing loose-typing errors in this
test file confirmed identical on a pristine upstream checkout).
- npm run lint -- clean.
⚠️ base-red inherited: #9985
|
||
|
|
3b41e795fc |
fix(translator): preserve Responses custom tools for OpenAI-compatible providers (#10114)
* fix(translator): preserve Responses custom tools * fix(translator): preserve Responses namespace tools * fix(translator): restore dotted namespace tool aliases --------- Co-authored-by: mtb-ninja <mtb-ninja@users.noreply.github.com> |
||
|
|
d259d9fcba |
fix(ci): clear base-reds on release/v3.8.50 (round 3) (#10213)
* fix(ci): clear base-reds on release/v3.8.50 (round 3) - CHANGELOG.md: restore the top [Unreleased] section dropped by the #10189 reconcile (docs-sync gate: first section must be Unreleased) - env-doc-sync: document CONDUCTOR_ORCHESTRATOR_TOKEN + CONDUCTOR_SPOKESPERSON_URL in .env.example/ENVIRONMENT.md; allowlist the CI-only GITHUB_STEP_SUMMARY and TS7_BASE_REF (ts7 ratchet signals); drop a stray merge artifact line - providers: restore the audited chatanywhere metadata entry that base-reds round 2 dropped together with its duplicate — the provider was half-wired (registry+endpoint without APIKEY metadata), which is what the wave3 test catches; re-pin providers-constants-split at the measured 228 - docs counts: 338 -> 339 (today's +2 void-ai/helixmind, -1 Puter) via gen:provider-reference + README/AGENTS/llm.txt/package.json/diagrams/i18n mirrors - file-size ratchet: annotated rebaseline for the two pre-existing drifts (ModelSelectModal 1138, gateways 1250) following the 2026-08-11 precedent Refs #9985 * fix(ci): base-reds round 3b — stale sibling tests + mode-pack weight contract - check-docs-counts-sync.test.ts: drop the imports/subtests of the four helpers #10196 removed from the gate script (readMcpFactsFromSource, listLocalizedDocs, makeRequiredCountsValidator, checkFreeTierInventory) — the new-API tests that #10196 added stay; the file now loads again under the node runner - quota-connection-recovery.test.ts: convert from vitest APIs to node:test — the file lives in tests/unit/*.test.ts (node-runner glob) and the vitest runtime crashes when imported outside vitest, killing the whole shard entry - modePacks.ts: re-normalize all six mode packs to sum 1.0 — #8940 added sessionAvailability: 0.05 to every pack without rebalancing (1.05 total); ratios preserved exactly (÷1.05), so post-normalizeScoringWeights behavior is unchanged; restores the declared sum-to-1.0 contract the 4235 test pins Refs #9985 * fix(ci): base-reds round 3c — vitest siblings, weights default, secrets FP, mutation tap - DistributeProxiesButton.test.tsx: wrap renders in NextIntlClientProvider — #9245 localized the component (useTranslations) and left the test without the intl context, failing all 14 cases - scoring.ts: re-normalize DEFAULT_WEIGHTS to sum 1.0 (same #8940 class as the mode packs — sessionAvailability added without rebalancing; ratios preserved) - .gitleaks.toml: generalize the kimi sponsor-banner localStorage-key allowlist to -v\d+ — #10200 bumped v1→v2 and the stale regex regressed the secrets ratchet with a false positive - stryker.conf.json: register 6 covering unit tests in tap.testFiles (4 modules) so their mutant kills count — unblocks check:mutation-test-coverage --strict Refs #9985 * fix(ci): base-reds round 3d — inspector factor gap, stale registry/gap tests, i18n key sync - comboScoringInspector: add cacheAffinity/sessionAvailability/connectionDensity to FACTOR_KEYS + the factor-key type — calculateScore() weighs them but the breakdown omitted them, so the explained contributions never summed to the reported score (inspector bug, red on the pure tip) - combo-scoring-inspector.test: make the explicit-weights override sum-neutral (±0.05 shift) so it stays valid for any DEFAULT_WEIGHTS values — the hardcoded override only summed to 1.0 against the pre-#8940 defaults, which is also why explicit weights silently fell back to 'default' on the tip - unorouter-registry.test: align to the canonical .com host (api.unorouter.ai 301-redirects there, verified live) and to wave4's live model discovery (passthrough, no static seed) — the .ai/auto-model expectations were stale - check-migration-numbering.test: 147 left KNOWN_GAPS when 147_api_keys_model_access_mode.sql landed — assert absent (same as 143) - i18n: sync-ui pass — 35,914 missing UI keys stamped as __MISSING__ placeholders across 42 locales (mechanical; greens the pt-BR key-presence integrity test; coverage pct unchanged by design — translation is a separate workstream) Refs #9985 * fix(ci): base-reds round 3e — 2 real defects + 14 stale sibling tests (waves A-E) Real defects fixed: - src/lib/db/apiKeys.ts: #9313's empty-allowlist early return bypassed the group permission check, silently disabling group deny rules (#8817) for every key without a per-key allowlist; fall-through restored, restricted+[] deny-all kept - open-sse/utils/proxyFetch.ts: #10032 re-appended the raw transport error to the propagated message, reintroducing the proxy user:password leak #9837 closed; new redactProxyDetailsInMessage() keeps the reason, redacts URL/credentials - .github/workflows/quality.yml: #10134 added the TS7 ratchet as a separate blocking step AFTER the aggregated gates — the exact #8542 masking mechanism; folded into the non-fail-fast loop (still blocking, still PR-only) ⚠️ CI edit, gate-strengthening — explicit owner sign-off requested on the PR - src/i18n/messages/ko.json: 3 machine-mistranslation regressions caught by the #8244 glossary checker (장애인→비활성화됨, 양말5://→socks5://, 비클로드→Claude가 아닌) Stale sibling tests aligned to deliberately-moved contracts (each cites its mover): request-log-detail-layout + -stream (#9245 intl provider), repro-8542 pin update, quality-rail-gate-membership (#10134 shape), agentSkills-routes 45→46 (#9058), cloudflare-ai-catalog-8717 (#8804 supersedes #8808), executor-xai (#9994), vision-bridge-claude-wire (#9463 minimax→openai), sse-auth forced-pin (#8893), tls-proxy-context (strengthened leak guards), rate-limit-local-error-classification (#9164/#9342), minimax-thinking-signature (#9463), codebuddy-cn (#9723 +1 test), github-copilot-custom-model (#9050), providers-g4f-batch3 (#9584), synced-capability-warmup (#9199, stricter), sidebar-tools-group (#8221), oauth-modal-grok-cli-paste (#9245); agentSkills/catalog.ts comment 45→46; file-size rebaseline for proxyFetch (+19, annotated) Refs #9985 * fix(ci): base-reds round 3f — waves F-J: 9 more real defects + stale sibling sweep Real production defects fixed (all red on the pure tip, each with its origin): - routeGuard.ts: #8949 accidentally DELETED the /api/providers/[id]/login local-only pattern — the route spawns a browser, so the loopback gate for a process-spawning route was gone (Hard Rules #15/#17); restored (314 guard tests green) - agentSkills generator: #9058's category dispatch gave the config category an empty body, wiping skills/config-codex-cli/SKILL.md at the #10131 sync; fixed + SKILL.md regenerated via the official generator - imageRegistry: #9982 broke same-provider bare aliasing (antigravity preview id sent upstream unresolved); new resolveSameProviderBareAlias() keeps the fal cross-provider fix intact - imageRegistry: #9982's prefix strip handed the bare nano-banana ids to fal-ai, violating the pinned 2026-07-31 operator decision (adobe-firefly owns them); fal entries made prefix-only (dispatch already re-prefixes) - mediaGeneration/fal.ts: the missing-credential 401 guard was lost when #10198 deleted the superseded falHandler — tests were hitting the live network - bottleneckPatch/rateLimitManager: #9041's merge clobbered #9604, resurrecting the Bottleneck v2.19.5 heartbeat bug (reservoir never refills); patched the library defect at the root and re-aligned chat-rate-limit-body-lock to the working reservoir contract - processSupervisor.mjs: #9761 regressed the Node spawn to bare "node" (the #9156 launchd bug) and dropped #9209's ipv4first args; both restored - openai-responses/pureHelpers: #9423's Agent null-sentinel was unreachable on the schemaless JSON-string path; gate extended - i18n en.json: #8222's regen reverted the #9976 unclosed-tag fix and #8559's combo-cooldown copy; #9038 shipped 40 t() calls with no messages (runtime MISSING_MESSAGE); all restored/added + official sync-ui stamps, and vi's zero-marker policy re-established via the sanctioned translation backend Stale sibling tests aligned (movers cited inline): chat-helpers (#9447), executor-antigravity (#9351), video-fal-grok (#9982), visionBridge (#9759), web-session-credentials (#8974), production-build-module-integrity (positive anchor added), agentSkills-generator/skillManifestsLint/skills-injection/ agentSkillTools-mcp/listCapabilities-a2a (#9058), memory-settings (#10010), model-catalog-policy-invalidation (#8906), model-alias-seed (#9485), reactive-context-compaction (#8949), combo-provider-wildcard (broken upsert helper), oauth-google-loopback (43-locale resurrected-key removal) Validation: 501/501 across the 47 touched test files; typecheck:core, lint, file-size, docs-sync all green. Refs #9985 * fix(ci): base-reds round 3g — wave K/L: 4 more real defects + stale alignments Real defects: - base/reasoningEffort.ts: the stale duplicate cherry-pick #9612 re-added the codex minimal→low rewrite that #9883 had deliberately removed (OMP minimal passthrough); block removed again - cursorImages.ts: #9840 wired prepareCursorImageForWire (sharp re-encode, fail-closed) into the SHARED resolveCursorImages, breaking zai-web and conol-web image uploads (HTTP 400 'undecodable'); new prepareForWire opt-out, Cursor default path unchanged (8 cursor suites green) - modelCapabilities/snapshot: catalog prepare still issued 323 per-model reads of model_context_overrides + max_input_tokens overrides, violating #9199's bulk-load contract; both now resolve from the snapshot single pass - v1-models-discovery-conformance: re-pinned to the bounded 30s SWR window (#9199/#10198) — the old 'stale-first regardless of age' contract is gone Stale tests aligned (movers cited inline): codex-tools-strict-default (#9828 redundant-oneOf strip), devin-providers (#9245 i18n), db-migrationrunner- constants-split (147→151 renumber #8228), gitlab-duo-oauth-setup (#9245), chatcore-extracted-modules (#9161 outbound-protocol keying) compression-api CI failures were cascade artifacts of codex-tools-strict-default failing in the same force-exit shard process — no own defect (171/171 local). Refs #9985 * fix(test): compression-api — register both describes before the runner starts The DATA_DIR setup + route/db top-level awaits sat BETWEEN the two describes; under --test-force-exit (the CI unit-runner flag) the process exits once the already-registered tests finish, so on slow CI machines the whole second describe died as 'Promise resolution is still pending' — the recurring CI-only shard-2 failure that never reproduced locally without the flag. Moved to the top of the file; 10/10 under --test-force-exit locally. Refs #9985 * fix(quality): freeze modelCapabilities.ts at 1006 (annotated) — snapshot routing growth Refs #9985 * fix(quality): move the modelCapabilities freeze into the frozen map (nested schema) Refs #9985 * fix(i18n): translate all 39,718 pending UI keys across 42 locales (owner-approved) Mass-translated every __MISSING__ placeholder via the official i18n:sync-ui --translate-markers pipeline (operator backend), restoring i18nUiCoverage to the 100 baseline (was 89.9 after the merge-storm UI landings + the 42 keys #9038 never shipped). Post-pass repairs, all caught by the existing gates: - glossary: retired renderings the machine reintroduced normalized again (提供商→提供者 zh-CN/zh-TW, 鏈接→連結, 文檔→文件, 調用→呼叫, 供應商→提供者, 響應→回應, 不活躍→未啟用 zh-TW; 클로드→Claude, 옴니루트→OmniRoute ko); DATA_DIR forbidden rendering avoided via 数据文件夹 rephrase - ICU integrity: 120 values with renamed/dropped {params} repaired (39 positional renames, 81 reset to the en source — functional over fluent) Validation: glossary/pt-BR/vi/deno-relay/settings-keys/value-drift/google- loopback suites 76/76; placeholder diff en×42 locales = 0; worst-locale coverage = 100.0%. Refs #9985 --------- Co-authored-by: backryun <bakryun0718@proton.me> |
||
|
|
b13c3cd202 |
fix(translator): normalize streamed optional tool arguments (#9423)
* fix: preserve Codex cache usage for Claude suggestions Co-Authored-By: Claude <noreply@anthropic.com> * fix: normalize streamed optional tool arguments Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Kittisak Tangsiri <kittisak@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
835de9d749 |
fix(sse): preserve Claude Code tool-name casing via Gemini/Antigravity (#9008) (#9016)
Stop blindly lowercasing PascalCase tool_use names on the Gemini→Claude path so Claude Code no longer rejects Read/WebSearch as missing tools. |
||
|
|
807a0d2022 |
maint: follow-up cherry-pick fix-in-place #9704 (conflict-resolved fallback) (#9889)
* fix(sse): persist per-tool-call JSON escape state across SSE delta chunks escapeJsonStringValues() reset its inString/pendingEscape state on every call instead of carrying it forward per tool-call index, so a raw newline byte (or an already-escaped \n) split across two delta chunks got corrupted in transit — the model's own output was correctly escaped, OmniRoute broke it. Root-caused via a dispatched investigation into real OpenClaw traffic that looked like model-generation quality but wasn't. Fix: escapeJsonStringValues now takes and mutates a persistent per-call state object (JsonStringEscapeState), keyed per tool-call index in the translator's init state and cleared when a tool call is superseded. * chore(quality): rebaseline openai-responses.ts for the escape-state fix Own growth from the extracted per-tool-call JSON escape-state fix (previous commit): open-sse/translator/response/openai-responses.ts 1204->1249 (+45). --------- Co-authored-by: Markus Hartung <mail@hartmark.se> |
||
|
|
05940f4c7f |
fix(responses-api): tool call after a text message collided on the same output_index (#9843)
Live incident (2026-08-08): an OpenClaw agent sent a short preamble line
("Kör nu, på riktigt — apply_patch på vibe-scriptet:") followed by an
apply_patch tool call in the same turn. The client only spoke the preamble
and never executed the patch, even though OmniRoute's own recorded
responseBody had a complete, valid tool_calls entry.
Root cause: emitToolCall/closeToolCall computed a tool call's output_index
as `reasoningIndex + 1 + tcIdx`, assuming reasoningIndex + 1 was free for
the first tool call (tcIdx=0). But a text message emitted in the same turn
ALSO claims reasoningIndex + 1 (or index 0 with no reasoning) — so a
turn with reasoning + text content + a tool call collided the tool call's
added/delta/done events onto the same output_index as the just-closed
message. A client that tracks response items by output_index (as expected
for the Responses API) sees the tool call events land on an index it
already marked complete and can silently drop them.
Fix: track whether a message item was actually emitted at that index
(state.msgItemAdded) and, if so, tool calls start one slot after it.
Extracted a shared toolCallOutputIndexBase() helper so emitToolCall and
closeToolCall can no longer compute this independently and drift apart.
Confirmed via the live call log artifact (id 1786223153235-770a1c):
response.output_item.done for the text message and response.output_item.added
for the tool call both carried output_index=1 in the raw SSE stream, 1.84s
apart, exactly matching the reported symptom.
Co-authored-by: Markus Hartung <mail@hartmark.se>
|
||
|
|
492f9ddc4a |
fix: buffer and normalize Responses tool-call argument deltas, stripping optional null before reaching the client (#9168)
Refs: base-red #9737 |
||
|
|
e7d9055314 |
fix(translator): avoid double-normalizing tool names in Gemini-to-Claude response path (#9177)
Refs: base-red #9737 |
||
|
|
c9a3361e5a |
fix(translator): add case-insensitive fallback for upstream tool call name lookups (#9575)
Closes #9575 |
||
|
|
c9debe92bd |
fix(translator): restore original tool name casing in Gemini response translators (#9568)
Closes #9568 |
||
|
|
e7f6b1d130 |
feat(radar): flag-gated signed free-model catalog overlay (#9515)
* feat(dashboard): add RADAR_ENABLED flag (default off) * feat(db): radar feed cache + settings with encrypted supporter key * feat(radar): signed feed sync with pinned key and version floor - feedSchema.ts: Zod v4 schema mirroring the server feed format (discriminated union on budget.kind, enum constraints, etc.) - pinnedKeys.ts: Ed25519 SPKI-DER pinned key + env override for forks - verify.ts: signature verification over exact wire bytes, never throws - sync.ts: full download/verify/validate/cache pipeline with injectable deps, feature-flag gate, opt-in gate, version floor (numeric compare), and sanitized error reasons (no stack traces) - 40 tests covering: contract hash, key handling, sig verification, schema validation, version compare, all sync paths (disabled, opt_out, invalid_signature, invalid_schema, stale, updated, error), auth header injection, and cache-untouched assertions for every failure mode * feat(radar): read-time overlay merge rules over the free catalog Pure function applyFeed() merges the cached Radar feed over the static baseline catalog at read time, honoring 4 rules: 1. Feed never overwrites a local override field. 2. enabled:false disables the entry with disabledBy:"radar" provenance. 3. User-added entry NOT in the feed survives untouched. 4. User deletion tombstone prevents feed resurrection. getRadarCatalog() accessor in index.ts: flag off / no cache / corrupt payload all fall back to baseline. Valid cache applies the overlay and returns feed metadata (version, tier, fetchedAt). TDD: 19 tests (4 rules + dedup + origin + accessor flag/cache/corrupt/ valid/bad-feed + baselineToMergedEntries converter). * feat(dashboard): radar catalog and guided setup screens - API routes: GET /api/radar/catalog, POST /api/radar/sync, POST /api/radar/settings - All gated on RADAR_ENABLED flag (404 when off) - Error responses via buildErrorBody(), never raw stack/message - Settings never echoes clear supporter key (masked omr_****<last4>) - Sync delegates to syncRadar() server-side, never proxies feed URL - Dashboard pages: - /dashboard/radar: 4 states (flag off, opt-in pending, empty, populated) - /dashboard/radar/setup?provider=X: guided setup with steps, key URL, test connection - Uses existing Card component and next-intl patterns - Sidebar: radar entry in costs group with icon - i18n: pt-BR and en keys for radarPage and radarSetupPage namespaces - Tests: - radar-api-routes.test.ts: 11 tests (flag-off 404, flag-on shape, error sanitization) - radar-page-state.test.ts: 5 tests (pure state logic) - All 90 radar tests pass (including prior 74) * docs(radar): module doc and flag-off inertia test Add docs/frameworks/RADAR.md covering the flag gate, the separate data-sync opt-in and privacy promise, the Ed25519 signature/pinned-key security model, tiers, the read-time overlay merge rules, and the self-hosting env vars — plus index entries in CLAUDE.md/AGENTS.md/docs/README.md/REPOSITORY_MAP.md. Document RADAR_FEED_URL and RADAR_FEED_PUBKEY in .env.example and docs/reference/ENVIRONMENT.md to satisfy check:env-doc-sync, which was failing on this branch since the sync.ts commit added the reads. Add tests/unit/radar-inertia.test.ts as the single canonical place asserting the "RADAR_ENABLED off => zero behavioral delta" claim end to end: the three /api/radar/* routes 404, the flag resolves to the definition default with no override, getRadarCatalog() returns exactly the baseline without touching the cache, and computeFreeModelTotals() keeps its pinned values with the Radar module imported alongside it. * fix(db): renumber radar migration to 135 after collision with 134 The base branch introduced 134_proxy_logs_egress_ip while this branch carried 134_radar_cache_settings; the migration runner rejects duplicate numeric prefixes. This migration has never been applied to a real database (the PR is unmerged), so no retroactive isSchemaAlreadyApplied guard is needed. * i18n(radar): translate radar catalog and setup strings to all locales The UI-coverage ratchet measures (present - placeholder) / total_en, so the __MISSING__ sentinels that i18n:sync-ui writes do not count as covered — only real translations restore the metric. Scoped to this PR's namespaces (radarPage, radarSetupPage, sidebar.radar*) instead of a bulk sync, which would have pulled ~978 unrelated pending keys into this diff. Placeholders and code identifiers verified preserved across all 1682 strings. * fix(radar): trust the served-tier header instead of the signed body field The signed feed body always carries tier:"live" by design (one signed artifact per version — rewriting the field server-side per request would break the exact-bytes Ed25519 signature). The server now returns the tier ACTUALLY served via the x-omniroute-feed-tier response header, so free users on a delayed community snapshot no longer see "Ao vivo (tempo real)" in the UI. sync.ts now reads and validates that header (falling back to the body's tier only when the header is absent or holds an unrecognized value) and stores the served tier in the cache; index.ts already surfaces cache.tier to the UI unchanged. * test(combo): shorten an assert message that exceeded the line limit The assertion added by #9507 was 104 chars, so prettier reformatted it into five lines on the next commit that touched the file, pushing it past its frozen size (3449) and failing check:file-size. The message is shortened (the issue reference stays in the comment directly above); the assertion itself is unchanged, and the file is back to 3448 lines and prettier-clean. * i18n(radar): use the canonical zh-TW glossary terms The machine translation produced retired renderings the glossary gate blocks: 供應商 for provider (canonical 提供者) and 文檔 for documentation (canonical 文件). Fixed across the 11 affected radar strings; tests/unit/i18n-glossary-consistency-check.test.ts is back to 17/17. * fix(radar): point the default feed URL at the domain that exists radar.omniroute.dev was a placeholder for a domain that was never registered, so an out-of-the-box sync would fail DNS resolution for every user. The live feed is served from radar.omniroute.online (the subdomain the design always specified), now behind Cloudflare TLS. Forks still override it via RADAR_FEED_URL. --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> |
||
|
|
7c0dba222c |
[v3.8.50] fix(sse): stop fabricating encrypted Codex reasoning summary text (#8807)
Validated in local merge-train T6 (ungrouped batch 1) |
||
|
|
8180b49ce1 |
fix(quality): 2 production bugs + 24 unit base-reds + measured gate ceilings (#9529)
* fix(quality): resolve net-new lint errors and allowlist #9343 assert rewrite Two `no-explicit-any` errors landed with #9407 and #9320 after the suppressions inventory was generated. Project policy is to fix new violations rather than freeze them, so both are typed instead: - #9407: `executor as unknown as Record<string, unknown>` - #9320: `(k: { name?: string })` Also allowlists the net-assert reduction in web-tools-translation-2820 (39->35). #9343 inverted the contract — bare JSON must no longer be promoted to tool_calls without an explicit <tool> envelope — so the tests were rewritten to assert non-promotion, which costs fewer asserts than validating a promoted object. More restrictive, not weaker. * fix(quality): raise integration ceiling to 40min and unpin codex-cli version in test The integration gate's 20min ceiling killed a healthy run: measured 22m08s hermetic on an idle 16-core box (935 tests across 112 files, strictly serial at --test-concurrency=1 because ~16 of them bind a port or share a DB). The "~3-10min" estimate in the code was stale by ~3x. 40min keeps the ceiling's real purpose — turning a genuine hang into a visible failure — without failing a long-but-healthy suite. Also fixes a base-red in chat-pipeline: |
||
|
|
3a1e42d985 |
fix(nvidia): normalize tool names and call IDs (#9236)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates — only pre-existing audit.test.ts flake). |
||
|
|
5cf9a33d85 |
feat(usage): surface Claude thinking token counts to clients (#9214)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates — only pre-existing audit.test.ts flake). |
||
|
|
c485c5e0a5 |
fix(sse): preserve Gemini thought_signature on Claude Desktop tool turns (#9015)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates: typecheck/complexity/cognitive/changelog/vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-222248-suite.log |
||
|
|
0a0fdad001 |
fix(translator): join reasoning summary segments with newline separators (#9500)
Closes #9500 |
||
|
|
455906c181 | fix(reasoning): forward Ollama Cloud thinking (#9290) | ||
|
|
f93da3f5ca |
Train 1D: merge via --admin on .113 validation
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass. |
||
|
|
11f7ba2c72 |
fix(sse): record tool calls into shared state for openai->openai-responses call-log summary (#8462)
The openai-responses translator tracked tool calls in its own funcCallIds/ funcNames/funcArgsBuf bookkeeping without ever writing to the shared state.toolCalls Map that stream.ts's completion-log summary builder reads. Every openai->openai-responses translated stream with a tool call was persisted with finish_reason "stop" and no tool_calls, even though the actual SSE events sent to the client were correct. |
||
|
|
ebe086ebb5 |
fix(sse): surface OpenRouter mid-stream error chunks instead of a false empty success (#8210)
* fix(sse): surface OpenRouter mid-stream error chunks instead of a false empty success OpenRouter (and similar OpenAI-compatible aggregators) can send an HTTP 200 SSE stream whose body carries a chat.completion.chunk with empty choices and a top-level error object instead of any delta -- e.g. the underlying provider hitting its own capacity limit mid-request. The Responses-API response translator's `!chunk.choices?.length` branch treated this exactly like a legitimate trailing-usage/no-op chunk, so the stream silently ended with response.completed / error: null / output: [] -- a false "successful but empty" response masking a real 502 provider_unavailable failure. Live repro: request 1784726796287-a45bb3 (OpenClaw via the default combo, nvidia/nemotron-3-ultra-550b-a55b:free via OpenRouter) got HTTP 200 with 0 tokens in/out after Nvidia returned "Worker local total request limit reached (33/32)" mid-stream; the client saw an empty completed response with no indication anything failed. Mirrors the Gemini mid-stream error fix (#4177): set state.upstreamError from the in-band error chunk so stream.ts's existing upstreamError handling takes over (sendCompleted emits status: "failed" with a real error object). Co-Authored-By: Markus Hartung <markus.hartream@gmail.com> * chore(quality): file-size baseline for own-growth (#8210) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Markus Hartung <markus.hartream@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
4ea08f520b |
fix(sse): Gemini malformed function-call handling + tool_choice translation (#8211)
* fix(sse): synthesize tool_calls for Gemini's malformed function-call abort reasons Live incident (dashboard log id 1784489701456-d8c0e9): Gemini terminates a stream with finishReason MALFORMED_FUNCTION_CALL/UNEXPECTED_TOOL_CALL when its own parser rejects an attempted tool call — there's no real functionCall part, only a human-readable finishMessage. gemini-to-openai.ts passed this through raw as finish_reason (9router#2462's fix, correctly keeping it off a clean "stop"/Claude end_turn), but a raw "malformed_function_call" isn't one of OpenAI's 5 documented finish_reason values, so a real OpenAI-format client (OpenClaw) has no handling for it at all and silently never notices the turn failed — confirmed live via tests/integration/live-gemini-workload.test.ts's [28] streaming case after the Gemini TPM/rebase work on this branch. Fix: synthesize a tool_calls entry (arguments carry the error code + Gemini's finishMessage, valid JSON) and finish_reason: "tool_calls" instead, routing the failure into the ordinary "tool call arguments didn't parse" path every OpenAI-compatible agent loop already handles. Defers to a real tool call if one already completed earlier in the same turn — the real call wins, no synthetic entry piles on top of it. Tests (TDD, each confirmed red-before-green): - 5 new unit tests in the existing 9router#2462 regression file, covering the synthesis itself, UNEXPECTED_TOOL_CALL, the real-tool-call-wins edge case, and no-regression on a clean STOP. - New fixture (tests/fixtures/translation/gemini-malformed-function-call-stream.json): the real 6-chunk event series from the live incident, sanitized (personal paths/URLs replaced with generic placeholders, structure preserved exactly). - New integration test chains the real translator into the real Responses API transformer using that same fixture, proving correct behavior on BOTH /v1/chat/completions and /v1/responses from one shared ground-truth event series. Co-authored-by: Markus Hartung <markus.hartung@gmail.com> * fix(sse): don't drop a malformed tool-call failure when it lands beside a real one Live incident (dashboard log id 1784589106014-2a42f8), analyzing why the prior malformed-function-call fix (3568c7259) still wasn't reaching the client in this case: Gemini can emit a REAL, valid functionCall AND finish the SAME candidate with MALFORMED_FUNCTION_CALL — the model attempted multiple tool calls in one turn (here: a real status-check call plus a malformed "exec"+"cron" multi-call attempt), one parsed cleanly and the other didn't. The first fix version skipped synthesizing a failure signal whenever a real tool call already existed (state.toolCalls.size > 0), on the assumption that meant the model was retrying a LATER, separate attempt after an earlier one already succeeded. That's indistinguishable, from the translator's state, from this same-turn case — so it silently discarded the malformed attempt's information entirely: the client saw the real call succeed and never learned the other tool calls were attempted and rejected. Fix: always synthesize the failure entry when a malformed abort reason is seen, appending it alongside any real tool call rather than skipping it. Multiple tool_calls in one response is normal, well-supported OpenAI behavior (parallel tool calls), so this adds the failure as an additional entry instead of replacing or hiding the real one. Tests (TDD, confirmed red-before-green): - Rewrote the unit test that encoded the old (wrong) assumption to assert both the real and synthesized calls are present. - New fixture (gemini-malformed-function-call-parallel-real-call-stream.json): the real event series from this incident, sanitized. - New integration tests (same file as 3568c7259's) prove both /v1/chat/completions and /v1/responses surface both tool calls correctly from this fixture. Co-authored-by: Markus Hartung <markus.hartung@gmail.com> * feat(sse): honor tool_choice when translating OpenAI requests to Gemini Investigating a live report that gemini-3.1-flash-lite frequently narrates an intended tool call in plain text instead of actually emitting one (dashboard log id 1784591483850-49c408 — 9 raw provider chunks, all plain text, zero functionCall parts, clean finishReason STOP): body.tool_choice was never read anywhere in the OpenAI->Gemini request translator. result.toolConfig was unconditionally hardcoded to { functionCallingConfig: { mode: "VALIDATED" } } whenever tools were present, regardless of what the caller sent. VALIDATED lets the model respond with plain text OR a schema-validated function call at its own discretion — it never forces a call the way OpenAI's tool_choice: "required" (Gemini's ANY mode) does, so a caller had no way to compel a tool call even when explicitly requesting one. Added convertOpenAIToolChoiceToGemini(), mirroring the existing convertOpenAIToolChoice() in openai-to-claude.ts for the same OpenAI tool_choice shapes (string "auto"/"none"/"required", or {type:"function",function:{name}} to force one specific tool): - unset/"auto" -> VALIDATED (unchanged default, no regression) - "required"/"any" -> ANY (forces a call) - "none" -> NONE (disables function calling) - {type:"function",...} -> ANY + allowedFunctionNames: [name] Wired into both Gemini request paths: the direct/base translator (openaiToGeminiBase) and the Antigravity/Cloud Code envelope (wrapInCloudCodeEnvelope), which now reuses the base translator's already- computed toolConfig instead of re-deriving its own hardcoded VALIDATED. This unblocks (but does not itself resolve) the live question — a tool_choice: "required" A/B test against gemini-3.1-flash-lite follows to confirm ANY mode actually changes the narrate-vs-act behavior in practice. Also updates the T11 any-budget allowlist for this file: the "any" string comparisons (tool_choice value "any", not a TypeScript type) are the same documented false-positive pattern already carved out for executors/base.ts. Tests (TDD, confirmed red-before-green): 9 new unit tests covering all tool_choice shapes on both the direct and Antigravity/Cloud Code paths, plus the no-tools and unset-default no-regression cases. Co-authored-by: Markus Hartung <markus.hartung@gmail.com> * chore(quality): file-size baseline for own-growth (#8211) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Markus Hartung <markus.hartung@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
07dada6b81 |
fix: strip internal reasoning placeholder from user-visible content (#8081) (#8162)
* fix: strip internal reasoning placeholder from user-visible content (#8081) The internal reasoning replay sentinel '(prior reasoning summary unavailable)' can leak into user-visible assistant content when a model echoes it through ordinary message.content / delta.content. Existing suppression only checked reasoning_content fields and reasoning-specific events. Changes: - Add stripInternalReasoningPlaceholder() to reasoningPlaceholder.ts — removes all occurrences of the sentinel and trims; returns '' when nothing meaningful remains - Streaming: strip in responsesTransformer.ts, openai-responses.ts, and openai-to-claude.ts at the delta.content entry point; skip emission entirely when only the placeholder was present - Non-streaming: strip in responseSanitizer.ts sanitizeMessageContent() and sanitizeResponsesMessageContent() (all three text paths) translateText is unaffected (uses mode='translate' via plain newsClient). The per-provider reasoning_content check remains as defense-in-depth. * fix: skip only empty content block on reasoning-placeholder, keep finish_reason/tool_calls (#8081) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(quality): rebaseline openai-responses.ts own-growth (#8081 guard) --------- Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local> Co-authored-by: Probe Test <probe@example.com> Co-authored-by: Dingding-leo <Dingding-leo@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
89bad0fa52 |
fix(responses): close namespace round-trip for Responses-Chat translation (#7936) (#8151)
* fix(responses): close namespace round-trip for Responses-Chat translation (#7936) The #7905 custom-tool-call path landed in release/v3.8.49 but left #7936 open: Responses namespace sub-tools were flattened to a bare leaf on the Chat wire with no response-side closure, so Codex's adjudicator rejected every namespace sub-tool call with `unsupported call` -- it only has a dispatch entry for the header bits (namespace+name), no entry for the bare leaf. This patch closes the round-trip without mutating the Chat wire name (alignment with #7905's bare-leaf contract and with the issue author's proposed fix): * request side (openai-responses.ts): keep tool.function.name as the bare leaf, populate a side-band namespaceToolIdentityMap keyed on that leaf, and thread it through translatedBody._toolNameMap. * request -> response seam (chatCore.ts): extract the identity map before dispatch and pass it through to the non-stream completion path and to all three stream pipelines (translate openai-responses, translate other, passthrough). * response translator (response/openai-responses.ts): in emitToolCall (response.output_item.added) and closeToolCall (custom_tool_call / function_call output_item.done), call resolveRequestToolIdentity() to rewrite the bare leaf back to {namespace,name} and emit codex-compatible independent fields. * passthrough (utils/stream.ts): add a response passthrough rewriter restoreResponsesPassthroughFunctionCallIdentity that intercepts response.output_item.added, response.output_item.done, and response.completed and stamps the same {namespace,name} tuple. * helper (requestToolIdentity.ts): a 20-line stateless resolver; never parses a name. The wire-visible Chat tool.function.name stays the bare leaf -- non-OpenAI providers (NVIDIA, GLM, Kimi, Gemini, ...) frequently truncate or rewrite long __-dotted names; bare leaves avoid that failure mode entirely. The codex ResponseItem::FunctionCall schema (models.rs) declares an independent namespace: Option<String> field and has a function_call_deserializes_optional_namespace round-trip test, so emitting it separately matches the codex adjudicator dispatch. Includes 19 new test cases across 4 files: - request-side bare-leaf wire + side-band ledger construction - response-side tuple emit + unmapped passthrough + apply_patch exclusion - ambiguous-leaf collision safety (entry dropped, leaf emits verbatim) - per-request isolation between concurrent streams - a precompiled Atlassian-style nested namespace override * fix(responses): skip tool_search_call input items instead of 400 (#7936 addendum) Codex 0.42+ emits `tool_search_call` (and later `tool_search_result`) input items when the model uses the dynamic tool-search optimization. They are metadata-only: they record that the model queried a subset of the available tools, and carry nothing that OpenAI Chat Completions can represent. Without an explicit skip in openai-responses.ts, the input loop threw Unsupported Responses API feature: input item type 'tool_search_call' cannot be represented in Chat Completions -- and because these items stay in the Responses API `input` for every follow-up turn, the whole server returned 400 on EVERY subsequent /v1/responses in the same session until the user cleared history. Observed in the wild: /v1/responses 400 "Unsupported Responses API feature: input item type 'tool_search_call' cannot be represented in Chat Completions [longcat/LongCat-2.0 (400), longcat/LongCat-2.0 (400)]" The meituan combo (longcat fallback) was the most visible victim, but the underlying throw is source-format-side and hits any Responses-API consumer whose upstream does not natively support Responses. Fix: stop on the item type the same way `reasoning` is skipped -- display-only metadata, no chat side-effect. Covers both `tool_search_call` and the follow-up `tool_search_result` shapes. Adds 3 unit tests: - tool_search_call is silently skipped (no 400) - tool_search_result is silently skipped - tool_search_call items interspersed with real messages are skipped in order; real messages survive * chore(quality): rebaseline openai-responses.ts + stream.ts own-growth (#7936 namespace round-trip) --------- Co-authored-by: TonPro <hello@tonpro.fu> Co-authored-by: RCrushMe <RCrushMe@users.noreply.github.com> |
||
|
|
eab59d4048 |
fix(translators): normalize TitleCase tool names for non-Anthropic models (#7926)
* fix(translators): normalize TitleCase tool names for non-Anthropic models * fix(translator): parse XML <invoke> blocks in OpenAI→Claude response translator * fix(gemini-web): mark gemini-3.1-flash-lite as toolCalling: false * fix(gemini-web): mark all models as toolCalling: false, add to no-tools combo * fix(web-providers): mark all web-cookie models as toolCalling: false * fix(nvidia): disable tool calling on models that can't handle it * fix(lint): drop explicit any annotation on extractXmlInvokeBlocks state param Removes the no-explicit-any violation introduced alongside the TitleCase tool-name normalization work so the merged branch stays lint-clean (state stays implicitly typed like its sibling helpers in this file). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
c1bdd91e7b |
Hide internal reasoning replay placeholders (#7912)
* fix: hide internal reasoning replay placeholder * fix: hide internal reasoning replay placeholder in OpenAI→Claude translation too Mirror the isInternalReasoningPlaceholder() guard already applied to the Responses-API and OpenAI-Responses reasoning paths in openaiToClaudeResponse() (open-sse/translator/response/openai-to-claude.ts). The internal reasoning-replay sentinel was still leaking into the Claude "thinking" content block on this translation path. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * test: close both-add merge of #7912 and #7905 reasoning/tool tests Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
6b59e814da |
Restore Responses API custom tool calls (#7905)
* fix: restore Responses custom tool calls * fix: preserve nested Responses custom tool calls * fix: reset superseded tool call state * fix: preserve tool precedence and buffered Responses tool arguments * test: verify top-level tool descriptions take precedence * fix: preserve custom Responses tool streaming semantics * test: cover declared custom tool streaming round trips * test: cover Responses custom tool metadata collection * test: cover active Responses custom tool stream * test: isolate Responses active stream regression * fix: complete Responses custom tool round trips |
||
|
|
747bce2f10 |
[defer] fix(grok): align responses tool-call shape for grok models (#6937)
* fix(grok): align responses tool-call shape for grok models * test(grok): cover responses tool call indexes --------- Co-authored-by: minisforum <no@mail.com> |
||
|
|
d6e86f413b |
fix(translator): synthesize tool call chunks from response.completed batched output (#7613)
* fix(translator): synthesize tool call chunks from response.completed output[]
When an upstream provider sends a batched response.completed event carrying
function_call items in its data.response.output[] array — without having
sent the individual response.output_item.added / .delta / .done events —
the state variables toolCallIndex and currentToolCallId were never set,
causing computeFinishReason to return 'stop' instead of 'tool_calls'.
This broke the agent loop for downstream Chat Completions clients
(OpenCode, Hermes, etc.) when routing through providers that batch their
output into the completed event.
Fix: parse data.response.output[] for function_call items in the
response.completed handler, synthesize the tool call header + arguments
delta chunks, advance state, and emit finish_reason: 'tool_calls'.
Also updates withAssistantRoleOnFirstDelta to handle array results.
Fixes #180, #3980
Refs: https://github.com/diegosouzapw/OmniRoute/issues/180
Refs: https://github.com/diegosouzapw/OmniRoute/issues/3980
* fix(translator): guard against double-emission for incrementally-streamed tool calls
Add a guard that skips response.completed synthesis for call_ids already
tracked via incremental output_item.added/.done events. Without this,
providers that stream incrementally AND echo function_call items in the
response.completed output[] snapshot get duplicate tool call chunks.
Also adds a regression test combining both incremental events and a
response.completed snapshot in the same turn.
Refs: diegosouzapw/OmniRoute#7613
* chore: add docker-compose.yml.bak to gitignore
* refactor(translator): extract response.completed synthesis, fix ratchets
Fixes the file-size and complexity/cognitive-complexity ratchet
regressions the dedup-guard commit (
|
||
|
|
046dad5bee |
feat(sse): add optional-enum null-omission idiom for codex strict-mode tools (#7023) (#7233)
Codex Responses API strict mode forces every "optional" tool property into `required`, so a model that intends to OMIT an optional enum property (no declared `default`, e.g. Agent.isolation: enum["worktree","remote"]) must still emit a concrete value. Neither of the two ops shipped in #6992 (drop-if-default, generalized drop-if-empty) can catch this: drop-if-default needs a declared default (none exists); drop-if-empty needs an empty string/array (the emitted value is non-empty). Adds a paired request/response transform scoped strictly to targetFormat === OPENAI_RESPONSES: - Request side: injectOptionalEnumOmissionSentinel/-ForTools widen no-default optional enum properties to accept `null` (OpenAI's own documented nullable-union idiom for this exact strict-mode limitation). - Response side: isDroppableNullEntry drops the key when the model emits `null` for a non-required, schema-declared property, reusing the existing #6992 toolSchemas plumbing (no new tracking structure needed). |
||
|
|
e9f784676d |
fix(translator): register openai response projection for gemini clients (#7207)
* fix(translator): register openai response projection for gemini clients
The response-translator registry had an OpenAI -> Antigravity response
projection registered, but no OpenAI -> Gemini one. When a client request is
detected as Gemini format (body-shape match on `contents: [...]`, per
detectFormat()) and combo routing lands the request on an OpenAI-native
provider, translateResponse() fell through its hub-and-spoke path with no
`openai -> gemini` translator registered, so the raw OpenAI
`chat.completion.chunk` shape reached the client unchanged instead of the
shared Gemini `response.candidates[]` envelope.
Registers FORMATS.OPENAI -> FORMATS.GEMINI reusing the existing
openaiToAntigravityResponse projection — Gemini and Antigravity already
share the same wrapped `{ response: { candidates: [...] } }` envelope
elsewhere in the pipeline (see the unwrapGeminiChunk callers in
open-sse/utils/stream.ts, which treat FORMATS.GEMINI and FORMATS.ANTIGRAVITY
identically), so no new conversion logic is introduced.
Co-authored-by: W ARELIK <warelik@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2399
* chore(changelog): fragment for #7207
---------
Co-authored-by: W ARELIK <warelik@users.noreply.github.com>
|
||
|
|
07e1011d3b |
fix(stream): reconcile encrypted Codex reasoning visibility without mutating upstream item (#7304)
* fix(stream): reconcile encrypted Codex reasoning visibility without mutating upstream item Resolves the collision between two open PRs on ensureVisibleResponsesReasoningSummary: #7095 (xz-dev) found that chat clients see nothing when Codex exposes reasoning only as encrypted_content, and added a visible placeholder — but did so by mutating item.summary in place. #7176 (JxnLexn) found that same mutation corrupts the forwarded response item, discarding the encrypted_content shape Codex needs for follow-up requests, and removed the mutation — but that also silently dropped the placeholder, so chat clients went back to seeing nothing. The mutation existed only so a later line could read the summary text back off the same item. getVisibleResponsesReasoningSummaryText() computes that text without touching the item, so: - synthetic response.reasoning_summary_text.delta / .part.done events still carry the placeholder for chat clients (#7095's goal), and - the forwarded response.output_item.done payload keeps its original encrypted_content intact with no fabricated summary field (#7176's goal). Applied at both call sites #7095 identified: the native Responses passthrough in stream.ts/passthroughTailProcessor.ts, and the Responses-to-Chat-Completions translator in openai-responses.ts. Closes #7095, closes #7176. Co-authored-by: Xiangzhe <xiangzhedev@gmail.com> Co-authored-by: Jan Leon <Jan.gaschler@gmail.com> * test(stream): guard the encrypted-reasoning mutation via the completed backfill path The output_item.done line is echoed verbatim on the wire, so a re-introduced item.summary mutation does NOT surface in that event — verified by re-injecting the mutation, which left the existing assertion green. The mutation does surface in the response.completed snapshot, where the captured reasoning item is re-serialized when upstream sends an empty output (store: false). Adds that case, which fails as expected when the mutation is re-introduced, making the #7176 half of the reconciliation an enforced regression guard rather than an incidental property of the current code path. Co-authored-by: Xiangzhe <xiangzhedev@gmail.com> Co-authored-by: Jan Leon <Jan.gaschler@gmail.com> --------- Co-authored-by: Xiangzhe <xiangzhedev@gmail.com> Co-authored-by: Jan Leon <Jan.gaschler@gmail.com> |
||
|
|
e078664ddd |
fix(sse): schema-aware optional tool-arg normalization for Codex routes (#6951) (#6992)
stripEmptyOptionalToolArgs was allowlist-only (Read/Subagent) and only stripped empty-string/empty-array values, so Responses API strict mode (every property forced into `required`) could forward a forced non-empty value (e.g. Agent.isolation) or a schema-declared default value verbatim to the client. Add schema-aware drop-if-default and generalized drop-if-empty (any tool, gated on schema.required), and thread each tool's JSON Schema from the request's tools[] into the two streaming call sites (response.output_item.done handling). Closes #6951 |
||
|
|
45d38bc4b2 |
fix(sse): defer response.completed until trailing usage-only chunk (#6906) (#6965)
Real OpenAI-compatible upstreams with stream_options.include_usage=true
send finish_reason in one chunk (usage: null) and the actual token counts
in a separate, trailing usage-only chunk (choices: [], usage: {...}).
Both the live translator (openai-responses.ts) and the legacy transformer
(responsesTransformer.ts) fired response.completed as soon as they saw
finish_reason, so the trailing usage chunk's token counts were captured
into state but never emitted -- Codex CLI and other /v1/responses
consumers saw response.completed with no usage field (permanent 0%
context-used).
Both translators now defer response.completed via an
awaitingTrailingUsage state flag when finish_reason arrives without
usage already captured, and complete on the next usage-only chunk (or
at stream end via the existing flush fallback) instead. Extracted the
duplicated events/emit boilerplate into a new
openai-responses/eventEmitter.ts leaf to keep the frozen
openai-responses.ts file under its file-size baseline.
Fixes 3 existing tests that encoded the old chunk ordering and adds a
permanent regression test (tests/unit/responses-usage-trailing-6906.ts)
covering both translators.
|
||
|
|
49e0b7d667 |
fix(responses): escape literal control chars in tool call JSON; emit … (#6786)
* fix(responses): escape literal control chars in tool call JSON; emit status=failed on upstream error #6785 Two bugfixes in the Responses API translator: 1. escapeJsonStringValues() sanitizes tool call arguments containing literal 0x0A/0x0D/0x09 bytes (emitted by Gemma4 models) into valid JSON \n/\r/\t escapes, preventing SSE framing corruption. Only escapes inside JSON string contexts — already-escaped sequences and structural JSON pass through unchanged. 2. sendCompleted() checks state.upstreamError and emits status="failed" with error.code + error.message instead of silently hardcoding status="completed" + error=null, so mid-stream errors (e.g. Gemini 503 after partial content) are properly surfaced to the client. 3. stream.ts: calls translateResponse(null,...) before controller.error() so the translator can emit close events (reasoning item done, response.completed) before the stream is terminated. * test(boundary): fix ESLint no-explicit-any warnings and quality gates Green the PR against release/v3.8.47 quality gates without weakening tests: - Replace @typescript-eslint/no-explicit-any in the new boundary/gemma4 tests with proper interfaces (ResponseBody, ToolDef, ToolArgs, SseEvent item accessors) — fixes the "No new ESLint warnings" gate. - Split tests/unit/translator-resp-openai-responses.test.ts (1079 LOC) by extracting the round-trip suite into a sibling file so both stay under the 800-line test cap — fixes check:file-size. - Rename the 5 live boundary tests to *.live.test.ts, gate them behind RUN_BOUNDARY_LIVE=1, add a test:boundary:live npm script and register the glob in check-test-discovery COLLECTORS — fixes check:test-discovery (they hit a live remote and must never run unopted in CI). Co-authored-by: Markus Hartung <mail@hartmark.se> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> |
||
|
|
1045e57aa1 |
feat(codex): echo requested effort-suffixed model id in Responses payloads (#3697) (#6820)
* feat(codex): echo requested effort-suffixed model id in Responses payloads (#3697) Codex CLI compatibility shim: the Responses API response.created/ response.in_progress/response.completed payloads now carry a `model` field (previously absent), and for Codex-CLI-originated requests it echoes the client-requested effort-suffixed model id (e.g. gpt-5.5-xhigh) instead of the bare upstream id (gpt-5.5), so the Codex CLI status line/model button shows the active reasoning effort. - openai-responses.ts translator threads the upstream model into the Responses event objects (additive, omitted when unknown). - New isCodexOriginatedHeaders() (codexIdentity.ts) reuses PR #3481's originator/User-Agent detection, header-based so it still fires when a combo routes codex/gpt-5.5-xhigh to a non-codex upstream. - chatCore's existing opt-in #1311 echoModel pipeline now also fires automatically for Codex clients on the Responses API, regardless of the echoRequestedModelName setting. - responseModelEcho.ts now also rewrites the nested response.model field the Responses API uses (previously only top-level model). - /v1/models keeps returning models: [] for Codex (unchanged, #3481). Regression guard: tests/unit/codex-effort-model-echo-3697.test.ts. Closes #3697 * chore(merge): re-sync with release/v3.8.47 (restore CHANGELOG, keep own bullet) * chore(merge): re-sync with release/v3.8.47; move changelog bullet to changelog.d fragment (merge-storm proof) |
||
|
|
6904484f51 |
fix(translator): defer content_block_start until GLM streams the tool name (#6730)
* fix(translator): defer content_block_start until GLM streams the tool name (port from 9router#2077) GLM 5.2 (and similar OpenAI-compatible upstreams) stream a tool call's id and function.name across separate SSE delta chunks. The openai-to-claude streaming translator emitted content_block_start immediately on the id-only chunk with an empty name; the Claude SSE protocol cannot patch a block after emission, so the later name-only chunk was dropped and Claude Code rejected the tool_use with an empty tool name / "No such tool available:". Defer content_block_start until the name arrives (start on args if they arrive first), and emit a start for any orphaned id-only tool call at finish so content_block_stop is never orphaned. Reported-by: itiwant (https://github.com/decolua/9router/issues/2077) * chore(6730): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) |
||
|
|
0969b56951 |
fix(translator): strip empty cloud_base_branch from Cursor Subagent tool call (#6729)
* fix(translator): strip empty cloud_base_branch from Cursor Subagent tool call (port from 9router#2446)
The Responses->Chat tool-arg cleanup (stripEmptyOptionalToolArgs) only stripped
empty-string/empty-array optional args for Claude Code's Read tool. Cursor's local
Subagent tool call therefore passed through with the cloud-only field
cloud_base_branch: "", which Cursor rejects ("cloud_base_branch may only be specified
when environment equals cloud") before starting the subagent. Extend the cleanup to an
allowlist of Read + Subagent; arbitrary tools stay untouched.
Reported-by: like3213934360-lab (https://github.com/decolua/9router/issues/2446)
* chore(6729): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)
|
||
|
|
2a52c402ce |
fix(antigravity): surface aborted Gemini tool calls off end_turn (#6713)
* fix(antigravity): surface aborted Gemini tool calls off end_turn Gemini/Antigravity aborts a turn with finishReason MALFORMED_FUNCTION_CALL (or a sibling like UNEXPECTED_TOOL_CALL) instead of completing cleanly. Both Claude-facing translators collapsed these to a clean end_turn, hiding the aborted tool call as a successful completion: - the OpenAI hub path (openai-to-claude.ts convertFinishReason default), and - the DIRECT Gemini->Claude path (gemini-to-claude.ts), which is the one Claude Code actually hits through an antigravity/Gemini-routed model. Add isAbortFinishReason() to finishReason.ts and map these reasons to tool_use on both paths; genuinely unknown reasons still fall back to end_turn. Co-authored-by: anhdiepmmk <n08ni.dieppn@gmail.com> Inspired-by: https://github.com/decolua/9router/pull/2462 * chore(6713): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) --------- Co-authored-by: anhdiepmmk <n08ni.dieppn@gmail.com> |
||
|
|
18fa0b2651 |
feat(providers): Gemini tool-calling end-to-end on /v1beta (#6222) (#6394)
Gemini tool-calling end-to-end on /v1beta (#6222) (net +1/-0, tests OK). Integrated into release/v3.8.46. |
||
|
|
1bda6c15dc |
Release v3.8.44 (#5925)
* fix(install): add pnpm-workspace.yaml allowBuilds + pnpm.json for pnpm 11+ pnpm 11 introduced ERR_PNPM_IGNORED_BUILDS for native addon packages. Without explicit allowBuilds approval, these packages silently skip build scripts and OmniRoute fails to start with missing native modules. Changes: - pnpm-workspace.yaml: Set allowBuilds=true for all 13 native addon packages (@parcel/watcher, @swc/core, better-sqlite3, core-js, esbuild, keytar, koffi, libxmljs2, onnxruntime-node, protobufjs, sharp, tls-client-node, unrs-resolver) - pnpm.json: Migrate onlyBuiltDependencies from package.json (deprecated field) to the new pnpm.json config file per pnpm 11 spec. Tested on: pnpm 11.9.0, Node 24, Windows 11. Fixes: pnpm install ERR_PNPM_IGNORED_BUILDS on fresh clone with pnpm 11. * chore(release): open v3.8.44 development cycle * test(security): parse Kimi Web URL host instead of substring match (CodeQL #689) (#5928) Alert js/incomplete-url-substring-sanitization: the Kimi Web executor test asserted result.url.includes("www.kimi.com"), which a hostile host like www.kimi.com.evil.net would also satisfy. Parse the URL and assert on the exact hostname (new URL(result.url).hostname === "www.kimi.com"), which is both a stronger check and clears the CodeQL warning. * refactor(translator): extract thinking-budget fitting from openai-to-claude (#5932) Extract the thinking-budget fitting cluster (fitThinkingToMaxTokens + private safeCapMaxOutputTokens + MIN_* constants) verbatim into the pure leaf openai-to-claude/thinkingBudget.ts. Host re-exports fitThinkingToMaxTokens so external importers keep working and imports it back for internal use. Host 822 -> 738 LOC (under the 800 cap). No behavior change: byte-identical bodies, public export set unchanged. Adds a split-guard test; all consumer tests stay green (translator-openai-to-claude, strip-empty, minimax-m3, passthrough). * chore(release): pipeline hardening — test-masking pre-flight gate + contributors/uncovered helpers (#5926) * chore(ci): add test-masking PR-context gate to release-green pre-flight Reproduce check:test-masking (vs origin/main) inside validate-release-green so non-allowlisted net-assert reductions surface in the local pre-flight instead of in a ~40-min CI layer on the release PR. run() now merges a per-gate opts.env so GITHUB_BASE_REF reaches the child. HARD gate; skipped under --quick. Context: v3.8.43 release cost 3 CI round-trips for PR-context gates (test-masking, file-size, pr-evidence) that check:release-green did not reproduce locally. * chore(release): add contributors generator + uncovered-commit reconciliation helpers - scripts/release/gen-contributors.mjs: reproducible `### 🙌 Contributors` table for a CHANGELOG version (parenthetical-group parser → accurate per-PR attribution, noise-handle denylist). v3.8.43 shipped without the section (a real miss) because it was hand-built. npm run release:contributors <version> [--inject]. - scripts/release/list-uncovered-commits.mjs: lists commits since the last tag with no CHANGELOG bullet (v3.8.43 had 123/176 uncovered at reconciliation start). Advisory, maintainer-side. npm run release:uncovered. - 20 unit tests (parenthetical attribution, noise exclusion, idempotent injection, coverage window). * chore(quality): absorb web-cookie-providers-new file-size drift from #5928 (base-red on release/v3.8.44) * refactor(translator): split openai-responses request translator into pure leaves (#5940) Extract the shared pure primitives and the chat->Responses direction out of the 894-line openai-responses.ts request translator: - openai-responses/helpers.ts: pure primitives (toRecord/toString/clampCallId/ normalizeVerbosity/etc + markers/regexes/JsonRecord), zero host imports - openai-responses/toResponses.ts: openaiToOpenAIResponsesRequest (chat->Responses), imports the helpers leaf Host keeps openaiResponsesToOpenAIRequest (Responses->chat, imported by production) plus both register() directions, and re-exports openaiToOpenAIResponsesRequest so external importers (tests) keep working. Host 894 -> 529 LOC (under the 800 cap). Verbatim bodies (multiset check: leaf A 54/54, leaf B 294 lines, fn1 intact), public export set unchanged, leaves never import the host (no cycle). Adds a split-guard test; all consumer tests stay green (responses-translation-fixes 37, verbosity 4, reasoning-effort 4, orphaned-tool-filter 8, empty-tool-name-loop 8, headroom-responses-format 3). * chore(ci): pr-evidence FAIL output tells you to push (body edit does not re-run the gate) (#5944) ci.yml ignores the 'edited' event, so adding the Evidence block to the PR body after a push does not re-run check:pr-evidence — you need another commit. The FAIL report now says so, at the exact place someone sees the red check. + 5 unit tests (classification + hint-on-fail / no-hint-on-pass). Decided against a separate edited-triggered workflow: pr-evidence is not a required check (no ruleset gates it; release PRs merge UNSTABLE, not BLOCKED), so the gap is cosmetic and the generate-release skill already puts Evidence in the body before the first push. * fix(providers): Perplexity Web emits real tool_calls in streaming mode (mirror chatgpt-web toolMode) (#5927) (#5937) Perplexity Web (Pro/Max) only converted <tool>{...}</tool> text into OpenAI tool_calls for non-streaming requests (hasTools && !stream). Streaming requests -- the default for agentic coding clients -- got the raw <tool> text as plain delta.content and never emitted a tool_calls SSE delta, so clients could not execute tools. Reuses the provider-agnostic buildToolModeResponse()/ toolCompletionToSseStream() helpers already shipped for chatgpt-web (#5240): when tools are requested, buffer the full completion and convert it into either a JSON completion or a terminal SSE replay carrying delta.tool_calls + finish_reason: tool_calls, regardless of the caller's stream flag. Extended buildToolModeResponse()'s idSeed to be caller-supplied (default 'cgpt', perplexity-web passes 'pplx') so tool_call ids stay provider-specific without duplicating the helper. Non-tool streaming is unchanged (still lives token-by-token via buildStreamingResponse). * fix(discovery): resolve duplicate /v1 paths and redirect aborts (#5904) Integrated into release/v3.8.44. Thanks @hamsa0x7 for diagnosing the doubled /v1 discovery path and the REDIRECT_BLOCKED probe-loop abort (#5899). De-scoped to the discovery fix (the #5903 session-affinity work is handled by #5943) and added Rule #18 regression guards. * docs(changelog): record #5926 + #5944 (release-pipeline hardening) under v3.8.44 Maintenance (#5952) * docs(claude): add Hard Rule #22 — cross-session safety (git stash + in-flight PRs) (#5955) Integrated into release/v3.8.44 — Hard Rule #22 (cross-session safety). * refactor(translator): extract pure helpers from response/openai-responses (#5949) Extract the 5 stateless helpers (normalizeToolName, stripEmptyOptionalToolArgs, normalizeOutputIndex, normalizeUpstreamFailure, extractResponsesReasoningSummaryText) verbatim into the pure leaf openai-responses/pureHelpers.ts (no stream state, no host import). Host imports them back and re-exports normalizeUpstreamFailure for external importers (tests). Host 1091 -> 1001 LOC. The stateful streaming core stays in the host (out of scope). Byte-identical bodies (multiset 73/73), no cycle. Adds a split-guard; consumer tests stay green (responses-translation-fixes 37, combo-param-validation-fallback-4519 5). * docs(compression): document upstream sync policy for RTK/Caveman engines (#5830) (#5948) Integrated into release/v3.8.44 — docs-only upstream sync policy for RTK/Caveman engines (closes #5830). All 7 checks green. * fix(sse): strip ANSI/VT100 codes from gemini-cli stream frames (#5934) Integrated into release/v3.8.44 — ReDoS-safe ANSI/VT100 strip for gemini-cli stream frames (port of upstream #2273, thanks @anki1kr). PR test green (5/5), file-size gate OK. * fix(translator): strict Anthropic content-block compliance in antigravity→openai request (#5935) Integrated into release/v3.8.44 — strict Anthropic content-block compliance in antigravity→openai (port upstream #2296). PR test green (9/9). UNSTABLE red is the pre-existing environmental setup-claude base-red (opencode-plugin dist not built in fast-path), not a regression from this PR. * fix(mcp): auto-recover stale streamable HTTP sessions on initialize (#5957) Integrated into release/v3.8.44 — MCP stale streamable-HTTP session auto-recovery (thanks @Chewji9875). * fix(providers): validate v0 Platform API keys via chats endpoint (#5954) Integrated into release/v3.8.44 — v0-vercel Platform API key validation (thanks @vittoroliveira-dev). * fix(api): relax provider-scoped chat completion validation (#5907) Integrated into release/v3.8.44 — relaxed provider-scoped chat validation + regression test (thanks @nickwizard). * fix(providers): strip /v1 unconditionally to avoid /v1/v1/models fetch error (#5899) (#5920) Integrated into release/v3.8.44 — unconditional /v1 strip in both models-discovery paths + regression test (thanks @anki1kr). * fix(resilience): per-window is_exhausted + honor quota-exhaustion preflight for priority combos (#5923) (#5941) Integrated into release/v3.8.44. * fix(resilience): honor active codex session affinity over per-request reset-aware re-scoring (#5903) (#5943) Integrated into release/v3.8.44. * fix(thinking): only inject redacted_thinking replay block when tool_use present and thinking enabled (#5945) (#5953) Integrated into release/v3.8.44. * feat(providers): add ClinePass API-key provider (#5942) Integrated into release/v3.8.44 — ClinePass API-key (BYOK) provider (port upstream 9router#2304, co-authored @adentdk). Validated locally: 16 clinepass tests green; fixed the APIKEY count 158→159 + translate-path golden snapshot (clinepass is a genuine new provider). Remaining UNSTABLE red is the pre-existing environmental setup-claude base-red (opencode-plugin dist not built in fast-path). Supersedes stub #5541. * feat(api): add /v1/ocr endpoint (Mistral OCR) + Mistral moderation (#5950) Integrated into release/v3.8.44 — /v1/ocr endpoint (Mistral OCR) + Mistral moderation (port upstream 9router#2064, co-authored @waguriagentic). Validated locally: 14 ocr-route tests + moderation/servicekind/endpoint-category suites green (CORS→Zod→handler + no-stack-leak assertion). Reds are inherited DRIFT only: cognitive-complexity ratchet (none from OCR files — pre-existing cycle drift, rebaselined at release) + environmental setup-claude base-red. * fix(codex): convert chat json schema to responses text format (#5933) Integrated into release/v3.8.44 — converts Chat Completions json_schema response_format → Responses API text.format on the Codex path, and preserves existing text.format through verbosity normalization. Base redirected main→release; the openai-responses.ts split that landed this cycle was reconciled by re-applying the delta onto openai-responses/toResponses.ts. Validated locally: 48 translator-openai-responses-req + 8 codex-verbosity tests green. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * feat(providers): add Claude Sonnet 5 support across the model pipeline (#5833) Integrated into release/v3.8.44 — wires claude-sonnet-5 end-to-end (registries, modelSpecs, pricing ×3, cost, Sonnet-family fallback, 1M-ctx, static models). Reconciled the add/add overlap with the already-merged #5796 (kept the PR's superset test with the family-fallback assertion). Validated locally: kiro-sonnet-5 + catalog + pricing/modelSpecs/fallback suites all green. Thanks @ggiak! Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * feat(relay): gate bifrost auto routing by provider manifest (#5870) Integrated into release/v3.8.44 — gates Bifrost auto-routing by the provider plugin manifest (only manifest-eligible providers reach the sidecar; ineligible/unknown fall back to the TS path with explicit reasons). Superset of #5869 (carries the full manifest + registry + docs). Resolved an integration-test conflict in favor of the release (which already subsumes this PR's readiness/removeDirWithRetry improvements). Validated locally: 4 provider-plugin-manifest + 11 relay-routing-backend tests green. Thanks @KooshaPari! Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * refactor(translator): extract pure message helpers from openai-to-kiro (852→751) (#5947) * refactor(translator): extract pure message helpers from openai-to-kiro Extract the pure tool/message helpers (parseToolInput, normalizeKiroToolSchema, serializeToolResultContent) verbatim into the leaf openai-to-kiro/messageHelpers.ts. The host imports them back for convertMessages. They were module-private, so the public export set is unchanged (no re-export needed). Host 852 -> 751 LOC. Byte-identical bodies (multiset 99/99), leaf has zero imports (no cycle). Adds a split-guard; consumer tests stay green (translator-openai-to-kiro 33, translator-ai-sdk-image-parts 3). * chore: re-trigger CI (stuck runner on 2/2 shard) * refactor(executors): extract pure prompt + composer helpers from cursor (#5960) Extract two pure clusters from the cursor executor into sibling leaves: - cursor/prompt.ts: isRecordLike + toolChoiceDirectiveLine + buildCursorOutputConstraints - cursor/composer.ts: composer thinking-as-content decoding (isComposerModel, visibleComposerContentFromThinking, composerReasoningRemainder + markers) Host imports both back for internal use and re-exports the 3 composer helpers for external importers (tests). Host 1576 -> 1451 LOC. Byte-identical bodies (verbatim multiset prompt 65/65, composer 32/32), leaves have zero imports (no cycle). Adds a split-guard; consumer tests stay green (cursor-composer-thinking, cursor-streaming, cursor-agent-tool-calls, translator-openai-to-cursor, cursor-agent-system-prompt). * refactor(executors): extract pure SSE-collect parsing from antigravity (#5962) Extract the pure SSE-payload -> collected-stream parser (AntigravityCollectedStream, stripZeroWidth, parseAntigravityTextualToolCall, addAntigravityTextualToolCall, processAntigravitySSEPayload/Text, flushAntigravitySSEText) verbatim into the leaf antigravity/sseCollect.ts. Host imports the helpers it uses and re-exports processAntigravitySSEPayload for external importers (tests). Host 1812 -> 1671 LOC. Byte-identical bodies (verbatim multiset 135/135), leaf does not import the host (no cycle). Credit/quota state, auth, and HTTP dispatch untouched. Adds a split-guard; consumer tests stay green (executor-agy 8, executor-antigravity 26, antigravity-sse-collect-socket-release, copilot-agent-antigravity-parity 6). * refactor(executors): extract pure model maps + resolvers from chatgpt-web (#5967) Extract the static model maps (MODEL_MAP, MODEL_FORCED_EFFORT, THINKING_CAPABLE_SLUGS) and the pure thinking-effort resolvers (isThinkingCapableModel, normalizeThinkingEffort, resolveThinkingEffort, ResolvedChatGptModel, resolveChatGptModel) verbatim into the pure leaf chatgpt-web/models.ts. Host imports the two resolvers it uses back. Host 3205 -> 3076 LOC. Byte-identical bodies (verbatim multiset 120/120), leaf has zero imports (no cycle). Auth/PoW/session/HTTP dispatch and all module caches untouched. Adds a split-guard; consumer tests stay green (chatgpt-web 86, chatgpt-web-tools-5240 4, chatgpt-web-sha3-boringssl-5531 5). * refactor(executors): decompose grok-web into pure tool/markup leaves (#5994) Extract the pure OpenAI<->Grok tool-translation, native-tool mapping, markup cleanup, and NDJSON stream types out of the 1872-line grok-web executor into 4 sibling leaves: - grok-web/types.ts: GrokStreamResponse/GrokStreamEvent (stream types) - grok-web/tool-bridge.ts: OpenAI<->Grok tool translation + registry + classifiers - grok-web/native-tools.ts: native-tool selection/scoring + native->OpenAI mapping - grok-web/text-cleanup.ts: Grok markup stripping + GrokMarkupFilter Layered, acyclic: types <- tool-bridge <- native-tools; text-cleanup <- types; host imports the leaves. All symbols module-private (no host re-export). Host 1872 -> 887 LOC. Byte-identical bodies (verbatim per-leaf), no cycle, all new leaves <= 800 cap (tool-bridge split at line 753 to stay under). Auth/cookie/TLS/HTTP dispatch untouched. Adds a split-guard; consumer tests stay green (grok-web 62, grok-cli-oauth 15, grok-cli-strip-params 2). * refactor(executors): extract pure quota parsing from codex (#5999) Extract the pure Codex quota-snapshot parsing + reset/cooldown scheduling (CodexQuotaSnapshot, parseCodexQuotaHeaders, getCodexResetTime, getCodexDualWindowCooldownMs) verbatim into the leaf codex/quota.ts. Host re-exports the 4 symbols so handlers/chatCore/codexQuota.ts + tests keep resolving. Host 1539 -> 1427 LOC. Byte-identical bodies (verbatim 98/98), leaf has zero imports (only Date, no cycle). WS transport, auth, HTTP dispatch untouched. Adds a split-guard; consumer tests stay green (executor-codex 40, codex-quota-fetcher 7, chatcore-codex-quota 5). * refactor(executors): extract pure stream formatters from deepseek-web (#6000) Extract the pure content/citation formatters (isThinkingModel, isSearchModel, cleanDeepSeekToken, formatStreamContent, DeepSeekSearchResult, appendSearchCitations) verbatim into the leaf deepseek-web/stream-format.ts. Host imports the 5 it uses back into transformSSE/collectSSEContent (cleanDeepSeekToken stays internal to the leaf). Host 1147 -> 1108 LOC. Byte-identical bodies (verbatim 34/34), leaf has zero imports (no cycle), all module-private (no re-export). PoW/auth/token-cache/HTTP dispatch untouched. Adds a split-guard; consumer tests stay green (deepseek-web 35, deepseek-web-rolling-window-2942 5, deepseek-web-tools-execute 3). * refactor(api): add validatedJsonBody helper (salvage #5075) (#5931) Fuses JSON body parsing + Zod validation into a single call that returns either type-narrowed data or a ready-to-return 400 NextResponse with the standard error envelope. Salvaged as the Tier 1 portable helper from the closed refactor PR #5075; the bulk route migration is intentionally not ported. Adds a focused 6-case regression test. Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> * feat(qoder): drive PAT auth via qodercli, add dashboard quota, fix connection display (#5816) Integrated into release/v3.8.44 — Qoder PAT auth via qodercli binary + dashboard quota + dual-auth connection fix. Thanks @AgentKiller45 (co-author @judy459)! Validated locally (release-green on its own merits): lint 0, typecheck:core 0, 104 qoder/usage/UI tests green, file-size gate OK (owner-approved qoderCli.ts baseline-freeze 666→989), env-doc-sync fixed (documented QODER_CLI_CONFIG_DIR). The 2 remaining CI reds are INHERITED base-reds, not caused by this PR: (1) LEDGER-4 minimax-m3 supportsVision (minimax-m3 base + cline-pass/minimax-m3 from the already-merged #5942); (2) mutation-test-coverage missing 3 tests in stryker.conf (#5903/#5942/#5923). Both cleaned up separately. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(providers): minimax-m3 supportsVision (LEDGER-4) + stryker tap.testFiles drift (#6012) Release-green cleanup — clears LEDGER-4 minimax-m3 supportsVision + stryker tap.testFiles drift base-reds. Validated locally. * fix(registry): flag cline-pass/minimax-m3 as multimodal (supportsVision) (#6003) The cline-pass provider's minimax-m3 entry was missing supportsVision, breaking the LEDGER-4 registry-consistency test (all minimax-m3 entries must set supportsVision to match lite.ts — minimax-m3 is multimodal). Every other minimax-m3 registry entry (trae, bazaarlink, cline, ollama-cloud, ...) already sets it. This was a base-red on release/v3.8.44 inherited by every open PR. Validated by the existing failing-then-passing guard tests/unit/review-reviews-v3814-fixes.test.ts (LEDGER-4). * refactor(executors): extract pure payload construction from claude-web (#6006) Extract the pure Claude-web payload types + transforms + default tools/style (ClaudeWebRequestPayload, ClaudeWebStreamChunk, DEFAULT_CLAUDE_MODEL, generateMessageUUIDs, getDefaultTools, getDefaultPersonalizedStyle, transformToClaude, transformFromClaude) verbatim into the leaf claude-web/payload.ts. Host imports the 3 it uses back (ClaudeWebRequestPayload type + the two transforms). Host 1056 -> 835 LOC. Byte-identical bodies (verbatim 149/149), leaf imports only randomUUID (no host import, no cycle), all module-private (no re-export). Cookie/auth/ Turnstile/TLS/HTTP dispatch untouched. Adds a split-guard; consumer tests stay green (claude-web 13, claude-web-auto-refresh 6). * refactor(executors): extract pure upstream-header helpers from base (#6008) Extract the pure upstream-header helpers (mergeUpstreamExtraHeaders, getCustomUserAgent, setUserAgentHeader, applyConfiguredUserAgent, isOpenAICompatibleEndpoint, stripStainlessHeadersForOpenAICompat) verbatim into the leaf base/headers.ts. base.ts is imported by ~18 executors, so the host re-exports all 6 to keep those import paths intact; it also imports the 4 it uses internally in the BaseExecutor class. The trivial JsonRecord type alias is redefined locally in the leaf to avoid a base<->leaf cycle. Host 1539 -> 1451 LOC. Byte-identical bodies (verbatim 78/78), leaf does not import the host (no cycle). typecheck:core validates all base importers still resolve via the re-export. Adds a split-guard; consumer tests stay green (executor-base-utils 22, executor-default-base 49, executor-strip-stainless-openai-compat 6, plus executor sanity via typecheck). * refactor(executors): extract pure wire protocol from perplexity-web (#6014) Extract the pure Perplexity wire protocol (consts, SSE stream types, SSE parsing, OpenAI<->Perplexity message translation, request/query builders, content extraction, sseChunk) verbatim into the leaf perplexity-web/protocol.ts. Host imports back the 10 symbols it uses; everything module-private (no re-export). Session cache, TLS fetch, auth, and the executor class stay in the host. Host 1028 -> 534 LOC. Byte-identical bodies (verbatim), leaf imports only randomUUID (no host import, no cycle). Adds a split-guard; consumer tests stay green (perplexity-web 26, streaming-tools-5927 2, tls-client 6, key-validation-models 2). * refactor(executors): extract pure URL normalizers from default (#6015) Extract the pure per-provider chat-URL normalizers (normalizeBailianMessagesUrl, normalizeDataRobotChatUrl, normalizeAzureAiChatUrl, normalizeWatsonxChatUrl, normalizeOciChatUrl, normalizeSapChatUrl, normalizeXiaomiMimoChatUrl, normalizeOpenAIChatUrl, getOpenRouterConnectionPreset) verbatim into the leaf default/urlNormalizers.ts. Host imports them back into buildUrl/transformRequest; the now-dead build*ChatUrl/normalizeBaseUrl imports move to the leaf. All module-private (no re-export). Host 864 -> 815 LOC (shrunk below its frozen baseline). Byte-identical bodies (verbatim 45/45), leaf does not import the host (no cycle). buildHeaders/execute/auth untouched. Adds a split-guard; consumer tests stay green (executor-default-base 49, anthropic-compatible-bearer 3, strip-client-metadata 3). * feat(webfetch): support self-hosted FireCrawl instances (#5793) Integrated into release/v3.8.44 — self-hosted FireCrawl support (FIRECRAWL_BASE_URL/FIRECRAWL_TIMEOUT_MS). Re-cut clean onto the release tip (branch was fossilized from a pre-v3.8.40 snapshot). Validated: 4 firecrawl tests green, env-doc-sync + docs-sync pass. UNSTABLE red is the inherited environmental setup-claude base-red. * feat(xai): register XaiExecutor with reasoning-effort suffix parsing (#5800) Integrated into release/v3.8.44 — XaiExecutor with reasoning-effort suffix parsing. Re-cut clean onto the release tip (branch was fossilized). Validated: 6 xai-executor tests green, provider-consistency OK, typecheck:core 0 errors, env-doc-sync in sync. UNSTABLE red is the inherited environmental setup-claude base-red. * feat(discovery): Phase 2 — reporter, /api/discovery/* routes (strict loopback-only) + dashboard UI (#5939) * feat(discovery): Phase 2 reporter — discoveryResults DB module + service wiring Adds src/lib/db/discoveryResults.ts (CRUD over the discovery_results table from migration 074) and wires the opt-in discovery service to persist and read findings through it: persistDiscoveryResult / getDiscoveryResults / getDiscoveryResultById / markVerified / deleteDiscoveryResult, with (provider, method, endpoint) upsert de-duplication. Re-exported from localDb. The service stays opt-in / default-off. The /api/discovery/* routes and the dashboard UI tab are intentionally deferred to Phase 2b — they need the local-only enforcement model (Hard Rules #15/#17 territory) decided first. TDD: tests/unit/db/discovery-results.test.ts (8 cases, DB + service delegation), isolated DATA_DIR with resetDbInstance cleanup. * feat(discovery): Phase 2b — /api/discovery/* routes (strict loopback-only) Adds the discovery HTTP surface on top of the reporter DB module: GET /api/discovery/results list findings (optional ?providerId) GET /api/discovery/results/:id one finding (404 if absent) DELETE /api/discovery/results/:id delete a finding POST /api/discovery/scan scan a provider + persist findings POST /api/discovery/verify/:id mark a finding verified Authorization: strict loopback-only. "/api/discovery/" is added to LOCAL_ONLY_API_PREFIXES so the central authz pipeline (proxy.ts → runAuthzPipeline → managementPolicy) rejects non-loopback callers with a 403 LOCAL_ONLY before any handler runs. It is deliberately NOT in LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES — no remote manage-scope bypass — because POST /scan issues outbound probes to provider endpoints (SSRF-adjacent) and must never be tunnel-reachable. Handlers also call requireManagementAuth (defense in depth) and return sanitized errors via createErrorResponse. Tests: - tests/unit/authz/discovery-routes-local-only.test.ts (8) — security guard: isLocalOnlyPath true + not manage-scope-bypassable for all four paths. - tests/unit/api/discovery-routes.test.ts (6) — handler integration over an isolated DATA_DIR: list/filter, by-id 200/404/400, scan persist + 400 on empty/malformed body, verify 200/404, delete 200/404, no stack-trace leak. * feat(discovery): Phase 2c — dashboard UI tab (Tools → Discovery) Adds the /dashboard/discovery page (DiscoveryPageClient) that consumes the Phase 2b /api/discovery/* routes: scan a provider, list findings, verify or delete them. Registered in the sidebar under the Tools group (icon travel_explore) and given a "discovery" i18n namespace + sidebar keys in en.json (other locales fall back to en via next-intl until synced — the locale files are in a pre-existing coverage deficit unrelated to this change). Registers the UI test path in vitest.config.ts (advisory ui suite). Tests: src/app/(dashboard)/dashboard/discovery/__tests__/DiscoveryPageClient.test.tsx (3 cases: loads+renders results, empty state, fetches /api/discovery/results on mount; stable useTranslations mock to avoid the fetch-loop). NOTE: the ui vitest suite cannot run in this workspace — @testing-library/dom (a @testing-library/ react peer dep) is absent from node_modules, which fails ALL existing ui tests equally; the test runs in CI. Component verified locally via typecheck + lint. * test(discovery): register discovery-routes-local-only in stryker tap.testFiles The mutation-test-coverage gate (--strict) flags any unit test covering a mutated module that isn't listed in stryker.conf.json tap.testFiles. This PR's tests/unit/authz/discovery-routes-local-only.test.ts covers src/server/authz/ routeGuard.ts (a mutated module, which this PR edits by adding the /api/discovery/ local-only prefix), so it must be registered for its mutant kills to count. No behavior change. * refactor(discovery): split DiscoveryPageClient to satisfy max-lines-per-function The complexity ratchet (max-lines-per-function: 80) flagged the single 184-line DiscoveryPageClient function (+1 over baseline). Extract the data layer into two hooks (useDiscoveryResults for list/loading/feedback, useDiscoveryActions for scan/verify/delete), a shared callApi helper, and two presentational sub-components (DiscoveryScanForm, DiscoveryResultCard). Every function is now under the 80-line ceiling; complexity gate back to baseline 1995. No behavior change — same exported component, same endpoints, same props. * test(sidebar): include discovery in omni-proxy item-order snapshot Adding the Discovery item to the Tools group (this PR's sidebar entry) extends the ordered omni-proxy section list. Update the exact-match deepEqual snapshot in sidebar-visibility.test.ts to include "discovery" in its position (after traffic-inspector). The assertion stays exact — this reflects the intentional new item, it does not weaken the check. * docs(changelog): restore release bullets eaten by merge auto-resolve; re-add discovery bullet additively * chore(quality): bump testFrozen for translator-openai-responses-req.test.ts (1097 -> 1172) Base-red inherited from #5933, which grew the test file to 1171 lines (Hard Rule #18 regression tests) without adjusting the frozen cap. The release tip itself fails check:file-size; this unblocks every PR into release/v3.8.44. File untouched by this PR. * chore(quality): restore stryker tap.testFiles entries eaten by merge auto-resolve The merge of origin/release/v3.8.44 silently dropped the 3 entries added on the release side (#5903, clinepass, #5923). Took the release version verbatim and re-added only this PR's entry (discovery-routes-local-only) in alphabetical order. check:mutation-test-coverage green locally. * chore(quality): reconcile inherited v3.8.44 merge-burst drift + include discovery in tools-group order test - complexity 1995->2003 and cognitive 856->859: both measure IDENTICAL on the pristine release tip ( |
||
|
|
604afeacf4 |
Revert "fix(config): externalize ws for copilot-m365-web executor (#6098, closes #6062)"
This reverts commit
|