mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 22:22:57 +03:00
12eef018cbb2ab40b0cd30bb8ce89360bd7b7f77
2396 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
12eef018cb |
fix(proxy): keep password-only proxy credentials (#10720) (#10752)
Co-authored-by: Markus Hartung <mail@hartmark.se> |
||
|
|
a1a37bbe7f |
fix: map OpenAI-compat voice names to real ElevenLabs voice_ids (#10589) (#10748)
handleElevenLabsSpeech forwarded body.voice straight into the ElevenLabs voice_id URL path segment with no name resolution, so OpenAI stock voice names (alloy, echo, ...) and ElevenLabs display names (Rachel, ...) 404'd upstream instead of resolving. Extracted the alias/display-name tables and resolution logic into open-sse/handlers/elevenLabsVoiceMap.ts (kept audioSpeech.ts under the file-size cap) and wired it into handleElevenLabsSpeech: known aliases resolve to a real voice_id, an omitted voice keeps the previous Rachel default, and anything unresolvable now returns a clear 400 instead of leaking an upstream 404. Co-authored-by: Markus Hartung <mail@hartmark.se> |
||
|
|
a8000bf1a2 |
fix: resolve audio provider short-alias prefix in parseAudioModel (#10586) (#10747)
Co-authored-by: Markus Hartung <mail@hartmark.se> |
||
|
|
e1c2425ed7 |
fix: de-list googleflow video provider and fail fast (#10285) (#10745)
The googleflow (Veo) video provider is live-confirmed broken on two independent axes: the submit/poll endpoints (/v1:generateVideo, /v1:fetchOperation) 404 on aisandbox-pa, and even the reporter's measured working endpoint (POST /v1/video:batchAsyncGenerateVideoText) rejects the stored Cloud Code OAuth bearer (401 UNAUTHENTICATED) since the cclog/cloud-platform scopes do not grant aisandbox-pa. Only a headed-browser reCAPTCHA session works (confirmed against gflow-cli's own docs), which cannot run headlessly. Exclude googleflow from getAllVideoModels() so it stops being advertised in /v1/models, and make handleGoogleFlowVideoGeneration fail fast with a clear diagnostic instead of forwarding to the known-wrong path and surfacing a raw HTML 404. Co-authored-by: Markus Hartung <mail@hartmark.se> |
||
|
|
cc544db38b |
fix: fail over streaming combo responses terminated with empty completions (#10404) (#10744)
validateResponseQuality's streaming-SSE peek only flagged an OpenAI-shape stream as invalid when it closed WITHOUT ever reaching finish_reason/[DONE] (#7285 truncation guard). A stream that DOES reach finish_reason: "stop" but never carries any real content, reasoning, or tool_calls in any chunk fell through as valid, exactly reproducing the reported content:null / completion_tokens:0 HTTP 200 for cmd/meta/muse-spark-1.2-contributor. Add a sibling failover branch for the terminated-but-empty case, mirroring the existing truncation branch. Tool-calls-only streams are unaffected — they already short-circuit through the earlier content-detection branch. Co-authored-by: Markus Hartung <mail@hartmark.se> |
||
|
|
41877978e0 |
fix: auto-replay bounded trajectory in DeepSeek Web prompt builder for non-tool-calling agentic clients (#10527) (#10741)
Co-authored-by: Markus Hartung <mail@hartmark.se> |
||
|
|
22b89a273b |
fix(config): keep the SQLite driver out of the client bundle (#10692) (#10695)
* fix(sse): import localDb through its real .ts extension (#10674) `open-sse/services/combo.ts` imported "../../src/lib/localDb.js" — a .js suffix on a module that only exists as .ts. Turbopack resolved it by accident until the dependency-tree change in #10647; after that the instrumentation hook died at boot with MODULE_NOT_FOUND, breaking `npm run dev` and the production build (60 consecutive red `Build App` runs on release/v3.8.50). Fixes the same latent pattern in src/lib/usage/usageLedger.ts, which survived only because it is an `import type` and is erased before resolution. Adds a guard rejecting relative .js specifiers across open-sse/ and src/. Package specifiers are untouched: publishing ESM as .js is legitimate there (e.g. @modelcontextprotocol/sdk), and only first-party relative imports are first-party TypeScript. Closes #10674 * fix(config): keep the SQLite driver out of the client bundle (#10692) The `aihorde` entry in IMAGE_PROVIDERS imported its live-catalog service directly. IMAGE_PROVIDERS is reachable from "use client" dashboard pages — they read its KEYS to derive which providers support which media kind — so that import dragged aihordeImageCatalog → safeOutboundFetch → proxyFetch → featureFlags → db/core → sqljsAdapter into the browser graph. The build then tried to bundle fs/net/tls for the browser and failed with 28 Module not found errors, leaving `Build App` red for 60 consecutive runs and no artifact buildable from the branch. A dynamic import() does not fix this: the bundler still has to make the module browser-loadable. The dependency is inverted instead — the registry entry knows only a pure registration module, and the server-only service registers itself on import, which every server path needing live models already does. With nothing registered the getter yields [], exactly what the live catalog returned before its first poll. Validated by a full `npm run build:release`: 0 Module not found, artifact produced. Closes #10692 --------- Co-authored-by: Xiangzhe <bakryun0718@proton.me> |
||
|
|
37c81ce1d7 |
fix(sse): import localDb through its real .ts extension (#10674) (#10691)
`open-sse/services/combo.ts` imported "../../src/lib/localDb.js" — a .js suffix on a module that only exists as .ts. Turbopack resolved it by accident until the dependency-tree change in #10647; after that the instrumentation hook died at boot with MODULE_NOT_FOUND, breaking `npm run dev` and the production build (60 consecutive red `Build App` runs on release/v3.8.50). Fixes the same latent pattern in src/lib/usage/usageLedger.ts, which survived only because it is an `import type` and is erased before resolution. Adds a guard rejecting relative .js specifiers across open-sse/ and src/. Package specifiers are untouched: publishing ESM as .js is legitimate there (e.g. @modelcontextprotocol/sdk), and only first-party relative imports are first-party TypeScript. Closes #10674 Co-authored-by: Xiangzhe <bakryun0718@proton.me> |
||
|
|
df90591415 |
feat(providers): refresh curated model catalogs and retire Imagen 4 (#10537)
* feat(providers): refresh Gemini Flash catalogs and pricing * fix(providers): refresh gemini-web Flash catalog * chore(providers): eliminate Gemini 3.5/3.6 Flash models * feat(providers): refresh Perplexity Web model mappings * feat(providers): refresh PromptQL and Notion catalogs * feat(providers): refresh KIE TinyCMS and Conol catalogs * feat(providers): refresh OpenCode Zen catalog * chore(providers): finish Gemini Flash cleanup * chore(providers): retire Google Imagen 4 |
||
|
|
04af8b1517 |
feat(compression): adota omniglyph 1.4.0, perfis semânticos e contabilidade com evidência (#10647)
* feat(compression): target-wire OmniGlyph stage and transport fidelity gate Roda o OmniGlyph depois da tradução para o wire real do provedor, em vez do corpo de origem. Um cliente OpenAI roteado para Claude deixava de comprimir com skip:source_format_not_claude porque o corpo ainda estava em formato OpenAI quando a engine era avaliada. - dispatch nativo por wire: Anthropic Messages, OpenAI Chat Completions e OpenAI Responses (input[] preservado, sem achatar para messages[]); - estágio target-wire pós-translateRequest, com guarda contra dupla compressão no caminho Claude→OpenAI; - preserveSystemPrompt do OmniRoute mapeado para compressSystem: false; - imageTransportPolicy: fidelidade de bytes/dimensões separada de supportsVision; só Anthropic/Claude tem recibo byte-preserving, o resto é fail-closed; - contagem de tokens de data URL PNG no wire OpenAI (marcador ;base64,); - README e i18n en/pt-BR com claims escopados ao caminho medido. * feat(compression): adota omniglyph 1.4.0 e tira o gate de modelo da env do host O 1.4.0 introduziu escopos de segurança e passou a resolvê-los dentro de isOmniGlyphSupportedModel() lendo process.env.OMNIGLYPH_PROFILE. Somado ao OMNIGLYPH_MODELS que já existia, duas variáveis do ambiente do host decidiam em silêncio o gate de TODO request do OmniRoute: passthrough desligaria a engine inteira e OMNIGLYPH_MODELS admitiria modelos sem recibo medido, enquanto a UI segue prometendo "Claude Fable 5 na rota direta medida". O adapter passa a usar isOmniGlyphSupportedModelForScope() com escopo explícito e fixa o escopo mais restrito como teto: a env só pode ESTREITAR a allowlist, nunca alargar. Os dois wires compartilham a mesma lista no pacote desde o 1.4.0, então uma checagem cobre Anthropic e GPT. - omniglyph ^1.3.1 -> ^1.4.0 (lock em 1.4.0); - testes de regressão para os dois caminhos de sequestro por env; - teste de contrato dos exports novos (escopo, perfis, accounting). O 1.4.0 também traz, sem mudança de código aqui: correção do glyph K que era lido como H, remoção do backtracking polinomial no secret-guard, overrides do pnpm em pnpm-workspace.yaml e as transitivas vulneráveis resolvidas. * feat(compression): expõe os perfis semânticos do omniglyph nos três wires O 1.4.0 trouxe perfis nomeados (coding-safe, balanced, aggressive, passthrough), mas só transformAnthropicMessages() os resolve sozinho: os transformadores OpenAI recebem TransformOptions cru e ignorariam o campo. Um perfil escolhido pelo operador valeria no wire Claude e sumiria no OpenAI. O adapter passa a mesclar o perfil com mergeCompressionProfileOptions() antes de chamar Chat Completions e Responses. O default segue aggressive — a política que os recibos publicados mediram. Medido nesta base: com coding-safe/balanced, uma sessão sem histórico acumulado para em below_min_chars e a engine não faz nada, porque os dois fixam minCompressChars no máximo e desligam system/tools/tool-results. Como a engine é opt-in, um default assim entregaria "ligado, 0% de ganho". O perfil é TETO, não piso: mergeCompressionProfileOptions não deixa um override do chamador reabrir uma lane lossy que o perfil fechou. Coberto por teste, por ser contra-intuitivo. Também fecha um caminho em que o OmniRoute violaria a própria política: o wire OpenAI do pacote não tem compressSystem — honra apenas compressTools, gptHistory, minCompressChars e reflow, e sempre troca a instrução por um ponteiro para a imagem. Com preserveSystemPrompt ligado, imagear assim queimaria o prefixo quente que a decisão cache-aware está protegendo, sem nada no corpo devolvido denunciando. A engine agora pula com skip:system_preservation_unsupported_on_wire. * feat(compression): contabilidade física do omniglyph com grau de evidência O adapter descartava o TransformInfo inteiro, então a UI mostrava um número de economia sem dizer de onde ele vinha — contagem do provider, estimativa ou só diferença de bytes. O 1.4.0 expõe normalizeAccounting(), que classifica essa evidência e resolve a semântica de cache por família: Anthropic reporta input, cache-create e cache-read em buckets DISJUNTOS, enquanto OpenAI e xAI reportam cached como SUBCONJUNTO do input. Somar à mão dá double-count silencioso. O novo omniglyphTelemetry.ts não filtra por denylist — MONTA um objeto novo, campo a campo, só com número e enum. TransformInfo mistura contadores inofensivos com material que não pode ser persistido: bytes PNG, imageSourceText(s), recoverable[].text, os sha8 de system/CLAUDE.md/primeira mensagem, nomes de tags observadas e o bloco env (cwd, branch, versões). Copiar o objeto inteiro transformaria telemetria de compressão em vazamento de prompt. O teste de negação prova que segredo, caminho do operador, texto do system e base64 não aparecem, e varre a allowlist exigindo que toda string seja de um enum conhecido. - provider threaded do chatCore e do bridge Codex WS até a engine; ausente vira `unknown`, que faz o upstream recusar adivinhar buckets de cache; - contabilidade propagada para o engineBreakdown do passo (o agregado do pipeline soma todas as engines e não serviria); - skip não emite contabilidade: zeros ali seriam indistinguíveis de "a engine nem rodou". * feat(compression): perfil do omniglyph configurável, persistido e documentado Fecha o caminho do operador: o perfil já existia no adapter, mas só como default de código. Agora atravessa schema Zod, normalizador do banco, API de settings e a página dedicada do engine. - OmniglyphConfig tipado + omniglyphConfigSchema (z.enum dos quatro perfis); - normalizeOmniglyphConfig: nome desconhecido vindo do storage cai para o default em vez de virar "roda com a política padrão"; - seletor na página do engine, com PATCH próprio — o perfil vive fora do mapa `engines`, e mandá-lo junto reescreveria o mapa inteiro (o store persiste o mapa como uma linha JSON só); - i18n en/pt-BR descrevendo o custo medido de cada perfil, não só o nome; - README e COMPRESSION_ENGINES.md com a regra do teto e o motivo de o default não ser o perfil mais seguro. Corrige de passagem um teste-irmão que ninguém via: o gate de transporte na UI deixou de dizer "direct Anthropic" quando os wires OpenAI nativos entraram, mas tests/unit/ui/omniglyphContextPage.test.tsx continuou afirmando a cópia antiga. O arquivo inteiro estava excluído do vitest.config.ts como "#8618 pre-existing failure", então a quebra passou silenciosa. Com a asserção alinhada o arquivo fecha 3/3, e a exclusão sai — o próprio comentário mandava removê-la quando corrigida. A doc não nomeia OMNIGLYPH_MODELS: o gate de docs fabricadas está certo em apontar que o OmniRoute nunca lê essa env — quem lê é o pacote. * fix(i18n): paridade do locale vi com as chaves novas do perfil do omniglyph `tests/unit/i18n-vi-completeness.test.ts` exige paridade ESTRITA de chaves entre en e vi — diferente do ratchet `i18n:check-ui-coverage`, que passa com 80%. As 11 chaves do seletor de perfil entraram só em en e pt-BR, e o gate de cobertura seguiu verde, então a quebra só apareceu na matriz completa do CI. --------- Co-authored-by: Xiangzhe <bakryun0718@proton.me> Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
233ac40e9d |
refactor(sse): ExecutorRegistry — route executor lookup through a runtime registry (R0.3) (#10633)
* test(sse): golden characterization of the executor map before the R0.3 registry refactor Freezes the 137-entry provider-id → executor mapping (class, provider identity, backing PROVIDERS config), the no-shared-instances invariant, and the getExecutor() dispatch rules (memoized DefaultExecutor fallback, cloud-agent guard #6699, search-provider guard #10274) as stable JSON snapshots. The upcoming ExecutorRegistry must keep both snapshots byte-identical. * refactor(sse): route executor lookup through ExecutorRegistry (R0.3) Adds open-sse/executors/registry.ts (Map-based registry mirroring translator/registry.ts): the built-in table in executors/index.ts stays declarative, every entry is registered at module load, and getExecutor()/hasSpecializedExecutor() resolve through the registry. DefaultExecutor fallback, its memoization, and the cloud-agent (#6699) / search-provider (#10274) guards are unchanged. Also fixes a latent lookup leak: the old object-literal lookup treated Object.prototype names (constructor, toString, ...) as specialized executors; the Map registry resolves them to the DefaultExecutor fallback like any unknown provider. Parity proof: executor-map golden (137 entries, byte-identical before/after), check:known-symbols green, 1018 tests across the 65 executor test files green. Docs: OPEN_SSE_ARCHITECTURE factory section corrected (it claimed generation from providerRegistry). Refs #3501 * test(executors): regenerate ExecutorRegistry golden snapshots after release sync release/v3.8.50 sunset mimocode and added cloudflare-playground + jina-search since this PR's snapshots were captured; refresh the golden fixtures to match. --------- Co-authored-by: Xiangzhe <bakryun0718@proton.me> Co-authored-by: Markus Hartung <mail@hartmark.se> Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
397a0e351e |
fix(compression): gate de estágio não derruba o pipeline com engine sem metadata (#10655)
`assertValidEngine()` valida id, apply, compress, getConfigSchema e validateConfig — não exige `metadata`. Uma engine registrada sem esse campo é, portanto, um registro legal. Mas `canRunAtCompressionStage` lia `engine.metadata.executionStages` sem guarda, então essa engine legal derrubava o pipeline inteiro com `TypeError: Cannot read properties of undefined` em vez de falhar aberto, que é o contrato da compressão. Metadata ausente é o mesmo caso de "não declarou executionStages" e passa a cair no mesmo fallback documentado: só pre-translation. Isso destravava também `tests/unit/compression/pipeline-circuit-breaker.test.ts`, que registra uma engine de teste sem metadata e vinha 8/9 na base — agora 9/9. O teste novo torna o contrato explícito, em vez de deixá-lo dependendo de uma reprodução incidental noutro arquivo. Co-authored-by: Xiangzhe <bakryun0718@proton.me> Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
885cd8c411 |
feat(gemini-web): expose image generation through /v1/images/generations (closes #10466) (#10494)
* feat(providers): add Cloudflare AI Playground as No Auth provider (closes #10389) Reverse-engineered access to the free, anonymous Cloudflare AI Playground: chat runs over a PartySocket WebSocket speaking Cloudflare's cf_agent RPC protocol with zero credentials (no account, no API key, no cookies). The WS upgrade is gated on a browser-grade TLS fingerprint, so the executor drives a headless Chromium via Playwright and speaks the protocol from inside the page context. - registry entry: cloudflare-playground (alias cfp), authType none, curated 20-model catalog (GLM 5.2, Kimi K2.7 Code, DeepSeek V4 Pro, gpt-oss-120B, Llama 3.3 70B, Qwen2.5 Coder 32B, ...) captured from the live getModels RPC (2026-08-15) - executor: cf_agent frame stream -> OpenAI SSE translation, id-filtered parser (RPC done:true frames cannot kill the stream), in-band upstream errors mapped to HTTP 429/502, abort + timeout handling, clean errors - noauth UI entry with reverse-engineered-endpoint notice - tests: 12 unit tests using real captured frames (incl. the 3021 rate-limit error) + fake transport; ESLint clean; open-sse typecheck clean * fix(providers): define __name helper in page context before evaluate Bundlers with keepNames (esbuild/tsx, webpack) inject a __name() call into serialized function bodies. page.evaluate(openPlaygroundSession) therefore threw ReferenceError: __name is not defined in real browser sessions. Define the helper on window before evaluating the session opener. * fix(providers): sync docs counts, golden snapshots and add reasoning_content support for cloudflare-playground * chore: remove ad-hoc cfp-shim debug script per review feedback The standalone shim duplicated the executor's frame-parsing and transport logic and is superseded by open-sse/executors/cloudflare-playground.ts. Requested in PR #10442 review. * feat(gemini-web): expose image generation through /v1/images/generations (closes #10466) Adds a gemini-web image-generation path following the chatgpt-web precedent: - imageRegistry: gemini-web provider entry (format gemini-web, cookie auth) with the nano-banana-web model. The -web suffix keeps the bare nano-banana id owned by adobe-firefly (operator decision 2026-07-31). - gemini-web executor: new parseStreamResponseImages() extracts generated image URLs from the StreamGenerate candidate extension block (inner[4][0][12][7][0], url at entry[0][3][3] — string or list form), dedupes cumulative frames, upgrades to =s2048, and deliberately skips web-search thumbnails at [12][1]. Image mode (x_gemini_web_image_mode) captures every StreamGenerate frame, resolves on first image, and gets a 90s window; chat mode is byte-for-byte unchanged. - handlers/imageGeneration/providers/geminiWeb.ts: drives the executor in image mode with an explicit generation directive prompt (the web UI otherwise answers with web-search images), caps n at 4, returns URLs or b64_json (downloads the public googleusercontent asset), and surfaces refusal text when no image was produced. - Dispatch branch on format gemini-web in handleImageGeneration. Tests: 21 new tests with fixtures built from the documented frame layout (string/list url forms, cumulative-frame dedupe, web-image exclusion, size-directive handling, refusal visibility, n-cap, b64_json, registry wiring incl. the bare nano-banana → adobe-firefly regression guard). Adjacent suites: gemini-web (6 files), chatgpt-web image, image handler, route, registry, adobe-firefly, freepik, designer — all green. ESLint clean on touched files (2 pre-existing any warnings unchanged); tsc -p open-sse 0 errors. * fix(media): close browser leak, surface timeout errors, and fall back accounts for gemini-web images Addresses pre-merge review findings on #10494 (closes #10466): - cloudflare-playground executor: close the launched browser on EVERY non-success start() path, including the detected Cloudflare "Attention Required" challenge branch (was leaking a Chromium process per blocked request). - cloudflare-playground executor: a streaming chat timeout now emits an explicit timeout_error SSE chunk before [DONE] instead of silently completing, so a client can no longer mistake an empty/partial timed-out stream for a successful answer. Timeout duration is now injectable for deterministic tests. - gemini-web image handler + imageCredentialRetry: classify the underlying GeminiWebExecutor's expired/blocked-session failure modes (400/500, per its own Playwright timeout/catch-all branches) as retryable, so executeImageWithCredentialFallback advances to the next eligible account instead of only doing so on a plain 401. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * docs: regenerate provider counts after merging release/v3.8.50 (341 -> 342) The previous merge commit resolved all 51 auto-generated-file conflicts by taking release/v3.8.50's content, which still said 341 providers. Merging in this branch's Cloudflare Playground provider brings the live catalog to 342, so npm run check:docs-counts-sync now flags stale claims. Fix: - docs/reference/PROVIDER_REFERENCE.md: regenerated via `npm run gen:provider-reference`. - README.md/AGENTS.md/llm.txt/package.json description: 341 -> 342. - docs/diagrams/{readme-hero,promise-pillars,comparison-table,cli-terminal}.svg: 341 -> 342 in the embedded "NNN providers" text (targeted replace, matched against the exact pattern check-docs-counts-sync.mjs validates). check:docs-counts-sync and check:changelog-integrity are both clean after this commit. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * docs(env): document CLOUDFLARE_PLAYGROUND_CHROME_PATH Used by open-sse/executors/cloudflare-playground.ts but missing from .env.example and docs/reference/ENVIRONMENT.md, caught by the env-doc-sync gate when combined with other PRs in the release merge-train. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: user.email <freakymustard67@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
beb6ec857b |
feat(dashboard): agentic conversation tracking — v4, decoupled + storage-architecture concern resolved (#10263)
* feat(responses): virtualize previous_response_id continuation regardless of upstream support OmniRoute now exposes OpenAI-compatible previous_response_id/store continuation to clients unconditionally, even when the selected upstream provider has no native Responses-API state support. Reconstruction happens server-side in handleChatImplementation, before any downstream validation or provider translation: OmniRoute resolves the response id back to the full input/output it previously produced, prepends it to the client's delta, and forwards the full reconstructed history upstream exactly as it does today. Client<->OmniRoute traffic shrinks to the new delta only; OmniRoute<->provider traffic is unchanged. Storage reuses the existing call-log pipeline artifact (already gated by call_log_pipeline_enabled, already retained/cleaned up by the existing call-log lifecycle) instead of duplicating conversation content into a second store -- only a lightweight call_logs.response_id index is new. Every lookup is scoped by api_key_id so one client can never resolve another client's stored conversation, and any unresolvable/missing/ size-limit-omitted state fails closed with OpenAI's own previous_response_not_found contract. Stacked on feat/openai-responses-store-toggle (#10121). * feat(dashboard): agentic conversation tracking with live transcript view Every agentic chat request now gets a conversation id (X-ConversationId response header). OmniRoute detects when a follow-up request continues the same conversation via fingerprint + bounded prefix-hash matching, with a strict-growth invariant to prevent false merges between independent single-shot requests that happen to share identical opening content. Continuation detection excludes the system message from the identity anchor, since real coding-agent CLIs commonly regenerate it every request with live context (timestamp, cwd, git status) — without this, that volatility alone broke every continuation check against real traffic. - `/dashboard/logs`: new toggleable Conversation column. - `/dashboard/logs/timeline`: requests sharing a conversation id share a timeline lane, connected by an arrow, with a configurable lane-reuse window. - Request detail panel: new Full Conversation transcript above the raw SSE event stream — Markdown rendering, per-turn timestamps, turn-relative view, click-any-turn navigation, live auto-refresh building the transcript in real time from the in-flight SSE chunk buffer while a request is still streaming, auto-scroll-to-bottom as the live turn grows. - New `/dashboard/conversations` page listing conversations with 2+ turns, no-forking model (an edited/duplicated mid-history turn mints its own independent conversation instead of merging), pagination, duplicate- anchor fix. - Configurable auto-refresh intervals on both the timeline and conversations list pages. - Responses API tool-call gap fix: turnsFromOpenAiMessages only handled role-based Chat Completions messages, so bare {type:"function_call"} / {type:"function_call_output"} / {type:"reasoning"} items (real Responses API traffic) silently vanished from the Conversation Context panel. - truncateForLog now counts input[] (Responses API), not just messages[] (Chat Completions), so a truncated /v1/responses request still shows a placeholder instead of nothing. - RequestTimeline.tsx now reads the same debugEnabled/emailsVisible settings RequestLoggerV2.tsx already used, instead of hardcoding both false — the timeline view never showed SSE/stream-chunk events or respected email-masking, regardless of the actual setting. Migrations 147/148 (agentic_conversations, conversation_turn_nodes) — 135 and 136 are now taken upstream; 143-145 are documented KNOWN_GAPS, so this uses the next free slot past upstream's current highest. Test plan: - npm run typecheck:core — clean - npm run lint — clean - node --import tsx/esm scripts/check/check-migration-numbering.mjs — OK, 0 collisions - 109 unit tests across the conversation-tracking, migration-renumber, and dashboard-wiring surface — 0 failures * refactor(dashboard): reuse call-log artifacts for conversation transcript content conversation_turn_nodes no longer stores turn text/tool-call content (text_preview/block_kind/tool_name) -- it's identity-only now (id/parent/ content_hash), matching agentic_conversations' existing lightweight-index shape. Every node's originating request is already fully captured by the call-log pipeline artifact its last_correlation_id points at, so the /dashboard/conversations tree view resolves each node's actual display content on demand from there (open-sse/services/conversationTurnContent.ts), re-running the same extractCanonicalTurns/hashTurnContent the write path used and matching by content_hash, instead of duplicating conversation content into a second store under a separate retention/gating policy. This also drops the old 8000-char text_preview truncation entirely -- resolved content is always full and untruncated. The frontend contract is unchanged (tree API still returns {textPreview, blockKind, toolName} per node), so the dashboard UI itself (page.tsx, RequestLoggerDetail/RequestTimeline, sidebar, i18n) needed no changes. Renumbered the cherry-picked 147/148 migrations to 153/154 -- 147 now collides with 147_api_keys_model_access_mode.sql, which landed on release/v3.8.50 after this work was originally built. Also includes a standalone, unrelated fix carried along from this rebase: close isProviderModelHidden's missing function-body brace in modelSelectModalHelpers.ts (separately landed as #10206). Stacked on feat/responses-previous-response-id-virtualization (#3), which is itself stacked on feat/openai-responses-store-toggle (#10121). * fix(dashboard): resync conversation list on open so the live-text poll starts immediately openConversation() seeded activeConversation (and therefore activeCallLogId, which gates the live-partial-text poll effect) from whatever row snapshot the list's own fixed-interval poll last produced. A conversation opened right after a reply started streaming -- after that tick, before the next -- had activeCallLogId still null, so the live-text poll never started; only a subsequent background list-poll resync (already existed) picked it up, which is why closing and reopening the same conversation "just worked". loadConversations() is now a shared callback so openConversation can force one immediately on open instead of waiting on pollSeconds. Live-verified against omniroute-dev: opening a conversation mid-stream now shows live reasoning on the first open. * style: prettier formatting for conversationTurnContent.test.ts * fix(db): close migration numbering gap left by decoupling from #3/#10262 153/154 (originally 154/155) were chosen back when this branch stacked on top of the previous_response_id migration (153_call_logs_response_id.sql). Decoupling removed that migration from this branch's history, leaving an unused 153 slot that check-migration-numbering.test.ts correctly flags as a gap. * refactor(dashboard): split RequestTimeline/RequestLoggerDetail under the 1000-line file-size cap Both files exceeded check-file-size's new-file cap after this PR's own additions (RequestTimeline 1048, RequestLoggerDetail 1163). Extracted pure non-component logic (types, constants, allocateLanes and its helpers) out of RequestTimeline.tsx into RequestTimeline.utils.ts, and the two self-contained presentational sub-components (PayloadSection, ConversationContextSection + its private helper) out of RequestLoggerDetail.tsx into RequestLoggerDetail.sections.tsx. No behavior change; existing external imports (default exports, allocateLanes, TimelineLog, CONVERSATION_LANE_REUSE_STORAGE_KEY) still resolve from the original file paths. * fix(db): renumber agentic-conversation migrations to clear 153 collision + sync migration-count docs The refresh-merge of release/v3.8.50 exposed that the feature's three migrations collided at slot 153 with the base's radar_local_model_state (153) and its own call_logs_response_id. Migration runner enforces unique numeric prefixes -> every DB init threw, red-ing Vitest, all Unit shards and the DB-backed quality gates. Renumber the feature's pair to 155_agentic_conversations / 156_conversation_turn_nodes and move call_logs_response_id to 154 (keeps 153_radar base-owned, preserves agentic-before-turn_nodes ordering). Update SQL headers and the 154/156 references in feature code + tests. Migration count is now 151 (was 148 stale in README/AGENTS/llm.txt) — sync the doc counts to clear the docs-accuracy gate. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(ui): drop unused CONVERSATION_LANE_REUSE_STORAGE_KEY re-export from RequestTimeline Knip 6.32 (baseline 415) flags the public re-export of CONVERSATION_LANE_REUSE_STORAGE_KEY from RequestTimeline.tsx as dead: no external consumer imports it through that re-export (it is imported and used directly from RequestTimeline.utils.ts inside the component). Removed the unused re-export; the internal import stays. DEAD_TOTAL 416 -> 415, back to the frozen baseline. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(agentic-conversations): guard resolveConversationId, drop dead whole-chain export - Wrap resolveConversationId() in try/catch in chat.ts, matching the defensive pattern used by every other best-effort side call nearby, so a DB hiccup in conversation tracking can't turn a working chat request into a hard failure. - Remove getConversationTurnTree: knip's project scope excludes tests/**, so an export used only by tests can never register as used there. Swap its 8 test call sites to the paginated getConversationTurnPage (already the dashboard's canonical query) with a generous limit, collapsing to one query path instead of keeping a second whole-chain export alive solely for test convenience. - Regenerate i18n llm.txt mirrors from root (pre-existing drift on this branch, unrelated to the above, caught by the docs-sync pre-commit gate). Addresses PR review feedback. * fix(i18n): close requestLogger conversation-column gap, fix domain-modules count drift - fr.json, vi.json were missing requestLogger.columns.conversation (added in the conversation-tracking feature), failing i18n-vi-completeness.test.ts. - docs/i18n/*/llm.txt mirrors still said 117 domain-specific files after an earlier rebase fixed the migration count but missed this companion number, failing check-docs-sync.mjs across all 42 locales. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(docs): restore PROXY_LOG_INCLUDE_IPS env/doc entries (env-doc-sync red) .env.example and docs/reference/ENVIRONMENT.md were both missing the PROXY_LOG_INCLUDE_IPS entry that src/lib/proxyLogger.ts already reads (confirmed present at this branch's merge-base too, so this predates the conversation-tracking work and is unrelated to it) -- the entry was added on release/v3.8.50 after this branch's last sync and this branch never picked it up. That gap red-lines tests/unit/check-env-doc-sync.test.ts and tests/unit/issue-7793-env-doc-sync-repro.test.ts (Unit Tests fast-path 2/4 in CI). Restore both entries verbatim from the current release/v3.8.50 tip -- no feature-code change. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: hartmark <hartmark@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
6615a5445b |
feat: combo-lane awareness + activation UX + MCP visibility (Wave 2 of #9654) (#10039)
* feat(admission): per-target lane-aware probes for combo/fusion fan-out (#9654 Wave 2)
Combo and fusion fan out N targets without ever consulting the adaptive-admission
layer: the parent request holds one lease, but each fan-out target is dispatched
unconditionally. With virtual lanes enabled (OMNIROUTE_CHAT_VIRTUAL_LANES=1), a
connection whose lane queue is full now SKIPS additional fan-out targets instead
of piling more queued work onto an already-congested session.
Adds PerTargetAdmissionHook (admission/types.ts) + createPerTargetAdmissionHook
factory (chatAdmission.ts): strictly non-blocking (maxWaitMs 0 - skip, never
queue), a no-op when virtual lanes are off, keyed to the parent tenantKey, and
release-on-admit so the probe is a capacity gate, not a hold.
Threaded through every parallel fan-out path:
- priority/weighted executeTarget + round-robin skip chains (combo.ts)
- fusion panel before fan-out (fusion.ts), judge fallback prefers survivors
- chaos parallel panel (autoCombo/chaosEngine.ts)
- tryFusionDispatch / tryRuntimeUnitDispatch / buildBaseOptions (dispatchPrelude.ts)
- chat.ts primary + safety-net redirect call sites
Snapshot exposes virtualLanes so the no-op gate is cheap and honest.
Tests: tests/unit/combo-lane-awareness-9654.test.ts (10 tests) - factory
semantics, priority/RR skip, fusion panel drop + all-skipped 503, no-hook
backward-compat baseline.
* feat(flags): activation UX - env-wins adaptive virtual-lanes flag + env docs (#9654 Wave 2)
U7: make adaptive virtual admission lanes discoverable + activatable.
- New OMNIROUTE_CHAT_VIRTUAL_LANES feature flag (boolean/runtime/requiresRestart) in featureFlagDefinitions + en.json i18n key.
- lib/admissionVirtualLanes.ts: env-wins resolver (env > DB > default) + boot warm folding a DB-sourced override into the process-global runtime env via reloadAdaptiveAdmissionRuntime(options.env) - no process.env mutation, no open-sse changes. Env still wins; DB toggle gates at next boot.
- GET /api/settings/feature-flags special-cases the flag to report the gate true source (ccDiscoveryAliases precedent); flagPayload helper dedupes the payload shape.
- Wire the warm into instrumentation-node registerNodejs (non-fatal, DB-ready).
- Document the master switch in .env.example + ENVIRONMENT.md with the system-1/system-2 distinction; zero new env-doc-sync drift.
- 11 new tests (resolver precedence + warm); 60/60 across feature-flag suites; typecheck core clean; ESLint + doc gates green.
* feat(mcp): surface adaptive admission lane data in omniroute_get_health (#9654 Wave 2)
U8: make adaptive virtual-lane admission visible to agents via the MCP health tool. handleGetHealth now surfaces a curated adaptiveAdmission block from the health payload (which already carried the runtime snapshot but was dropping it): virtualLanes/pressure/utilization/laneCount/laneQueuedCount/laneQueuedCost, laneTenants capped at top-10 by queued cost, admitted/rejected/wouldReject counts, shutdown. Block omitted entirely when the health endpoint reports none.
isLaneFlagOn mirrors the runtime 1|true convention so a string serialization can never invert a boolean lane report. getHealthOutput schema extended with the matching optional shape; tool description updated.
4 new dispatch tests (full block, top-10 cap/order, omission, defensive coercion of string flags + malformed lane entries) - 22/22 in essentialTools.test.ts. README: Adaptive Admission Lane Data table + Skills & Tool Navigability audit (29/43 schema entries covered, 14 undocumented, tool_search keyword runtime discovery, full catalog in docs/frameworks/MCP-SERVER.md).
No new lint errors (4 pre-existing in server.ts), typecheck core clean, doc counts + fabricated-docs gates green.
* docs: add changelog entry for #9654 Wave 2 (#10039)
* fix(codeql): suppress js/insufficient-password-hash false positive in lane-key fingerprinting (#10039)
resolveSessionId sha256-hashes bearer/x-api-key/x-goog-api-key to derive a deterministic, non-reversible per-key lane-bucket ID for virtual admission lanes (#9654). This is not password storage or verification, so the rule is a false positive; suppress it inline (same house style as src/lib/sync/tokens.ts) to clear the codeqlAlerts ratchet (2 > baseline 1) that blocks #10039 and every PR against release/v3.8.50.
* docs(mcp): complete MCP server README tool reference (#10039)
The MCP server README covered only 29 of the 43 schema entries, listing the
remaining tools solely as a gap note with omniroute_tool_search as the runtime
fallback. Add tool-reference tables for the agent-skills trio, oneproxy trio,
web_fetch/web_search, tool_search, create_combo, set_routing_strategy,
pick_fastest_model, sync_pricing, and db_health_check so the README covers the
full schemas catalog, and fold the coverage note into the tool_search discovery
paragraph.
* fix(chat): drop unused correlationId from safety-net combo redirect (#10039)
handleComboChat's HandleComboChatOptions has no correlationId member and
the combo pipeline never consumes it; the property was copied from the
handleSingleModelChat options shape by accident and introduced a new
TS2353 under the open-sse workspace typecheck gate.
* fix(i18n): translate featureFlagChatVirtualLanesEnabledDescription into 42 locales (#10039)
en.json gained the flag description in this PR but the locale catalogs
were never mirrored, failing the pt-BR key-parity (#6695) and vi
completeness gates. Adds a real translation to every locale, keeping the
zh-CN/zh-TW glossary canonical terms (提供者/儀表板) and no ICU drift.
* chore(quality): ratchet open-sse-typecheck baseline down (#10039)
The Wave 2 admission refactor removed 66 baselined open-sse type errors;
re-freeze the baseline so the gate pins the new, tighter state.
* docs: resync provider reference to 341 and CLI tools to 34
The release branch gained an 11th no-auth provider (freeaiapikey registry
resync, #10233) and a 26th CLI Code tool without regenerating the
auto-generated docs, leaving every PR against release/v3.8.50 failing the
Docs Gates strict validator (code 341 vs doc 340, CLI 34 vs "33 tools").
Regenerate docs/reference/PROVIDER_REFERENCE.md and sync the provider/tool
counts across README.md, AGENTS.md, llm.txt plus 42 i18n mirrors,
package.json description, and the four diagram SVGs.
* fix(tests): align count expectations with live catalogs (pre-existing release drift)
Release/v3.8.50 currently fails five gates on its own tree; this PR inherits
them. Fix the stale expectations to match live code:
- feature-flags-settings: 48 -> 49 flags (Wave 2 adds OMNIROUTE_CHAT_VIRTUAL_LANES)
- cli-tools-schema / cli-catalog-counts: 33 -> 34 tools (zcode added; 26 code = 21 visible + 5 none)
- optional-transformers-dependency: onnxruntime-node ~1.24.3 -> ~1.27.0 (bump #10382)
- stryker.conf.json: register chatcore-header-drop-warn-dedupe-10315 test
- check-public-creds: freeze zcodeProtocol clientId false positive (client identifier, not a credential)
* fix(tests): follow release's onnxruntime-node revert to ~1.24.3
release/v3.8.50's #10543 pinned onnxruntime-node back to ~1.24.3 after
#10403's ~1.27.0 bump caused npm to nest a second native copy under
@huggingface/transformers and broke the Docker SONAME contract. This
PR's own drift-alignment commit (
|
||
|
|
8acd799af7 |
feat(routing): add exclusive managed session connection leases (#10362)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
3cab6dc9f0 |
fix(combo): resolve nativeCodexTurnPin type error and connection-pin gap
PR #10573 landed with two real defects surfaced by typecheck/tests on the combined release tip: - TS2322: allowedConnectionIds (string[]) was built from compatible.map(t => t.connectionId), whose type includes null. Filter nulls before assigning. - applyNativeCodexTurnPin never assigned the pinned connectionId onto a compatible candidate that didn't already carry it (e.g. an unresolved placeholder target with connectionId: null) — the pin was silently dropped instead of applied. Now resolves the pinned slot's connectionId explicitly (in original order, so allowedConnectionIds stays consistent regardless of pinned-first reordering) before building the returned target list. Confirmed via the existing focused suites: tests/unit/chatgpt-web-codex-turn-pin.test.ts and tests/unit/native-codex-turn-pin-10379.test.ts (14/14 pass), typecheck:core clean. |
||
|
|
fb89fafc3a |
fix(backend,combo,cursor): header budget, Codex failover, kv_after_text (#10573)
* fix(combo): allow fill-first failover across Codex OAuth connections applyNativeCodexTurnPin previously narrowed the target pool to the single pinned connection, making same-provider failover impossible when the pinned connection was rejected by pre-dispatch checks. Return all compatible connections (same provider + model) with the pinned connection first, so the combo engine can fall over to siblings. Also allow pinNativeCodexTurn to update connectionId for failover recovery while still rejecting provider/model changes. Fixes #10379 Signed-off-by: Minxi Hou <houminxi@gmail.com> * fix(test): replace as any with properly typed ResolvedComboTarget literal Addresses ESLint no-explicit-any error in tests/. Signed-off-by: Minxi Hou <houminxi@gmail.com> --------- Signed-off-by: Minxi Hou <houminxi@gmail.com> |
||
|
|
6b823aa441 |
fix(logging,sse): redact sensitive log fields and default SSE comments to disabled (#10539)
* fix(logging): redact client IPs and account prefixes by default ProxyEgress and AUTH logs exposed client IPs, egress IPs, and account prefixes at info level — a privacy leak in multi-tenant/shared-log environments. Now redacted by default, only shown when debugMode=true. Fixes #10348 * fix(sse): default SSE comment lines to disabled Strict SSE clients (WorkBuddy, etc.) JSON.parse every SSE line and crash on comment lines. Changed OMNIROUTE_SSE_COMMENTS default from enabled to disabled. Operators can opt in with OMNIROUTE_SSE_COMMENTS=on. Fixes #10524 * fix(logging): gate AUTH account-prefix redaction on a narrow flag, not debugMode The proxy-log redaction half of #10348 is superseded by an already-merged fix (PROXY_LOG_INCLUDE_IPS, decoupled from debugMode). The remaining gap was the chat.ts AUTH log line ("Using <provider> account: <prefix>..."), which this PR gated on the broad `debugMode` setting. `debugMode` is a general dashboard-visibility toggle unrelated to log privacy — coupling redaction to it means any future, unrelated change to debugMode's default silently changes whether account prefixes leak into logs. Add a dedicated AUTH_LOG_INCLUDE_ACCOUNT_ID feature flag (default off, security category) and gate the AUTH log line on it via isFeatureFlagEnabled(), which reads the DB override synchronously on every call (no stale in-memory cache to invalidate) and fails safe to redacted on any lookup error. Also update the SSE-comments tests/docs that still asserted the old enabled-by-default behavior (tests/unit/sseHeartbeat.test.ts, tests/unit/sse-comments-optout-9305.test.ts, docs/reference/ENVIRONMENT.md) to match the new default-off behavior from this PR's earlier commit. Refs #10348, #10524 Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
c545855b26 |
fix(logging): capture early-keepalive bytes in the call-log artifact (#10331)
Diagnosed while chasing the reused-output-index incident (see 705ac7335 / OpenClaw issue #123342): every call-log artifact showed a wire-clean response, even for requests that actually failed, because withEarlyStreamKeepalive injects its startup/keepalive/error frames directly into the outer response stream, entirely outside the request handler's own reqLogger. reqLogger.appendConvertedChunk (which populates pipeline.streamChunks.client) never sees those bytes — only what chatCore.ts's own SSE writer produced. The persisted artifact was answering "what did the handler generate," not "what did the client actually receive," which is the wrong question when diagnosing a client-visible stream defect. withEarlyStreamKeepalive wraps the handler's Promise from OUTSIDE its call tree; the reqLogger it needs to feed is created deep inside chatCore.ts, after routing/model/provider resolution, and doesn't exist yet when the keepalive frames are written. The two sides share no reference — only an identifier, if one is deliberately threaded through both. Fix: responses/route.ts now generates a correlationId before calling handleChat, passes it as handleChat's existing (already-supported, previously-unused-here) 4th positional arg — which chatCore.ts already threads into trackPendingRequest's metadata as entry.correlationId, zero changes needed there — and also into withEarlyStreamKeepalive's options. The wrapper buffers every direct- to-client write (startup frame, periodic ticks, in-band error frames) via the new earlyKeepaliveByteBuffer module, keyed by that same id. chatCore/attemptLogging.ts, which already has correlationId in scope right where it assembles the final pipeline payload before saveCallLog, takes the buffered bytes and prepends them into streamChunks.client in send order. The verbatim-forwarded real response body is deliberately NOT re-recorded here — the handler's own reqLogger already captures that; recording it twice would duplicate it in the artifact. The buffer is consumed exactly once per correlationId and swept on a 10-minute TTL so a request that never reaches the persist call (aborted, detailed logging disabled, a route that doesn't opt in) cannot leak entries forever. Scoped to /v1/responses only, where the incident actually happened. /v1/chat/completions and /v1/messages call withEarlyStreamKeepalive the same way and would need the identical two-line route change to opt in; left as a follow-up rather than bundled in sight-unseen. Test plan: - tests/unit/early-keepalive-byte-buffer.test.ts (new): record/take ordering, single-consumption, per-id isolation, empty-input no-ops, unbounded-growth cap - tests/unit/early-stream-keepalive.test.ts: two new tests — a correlationId records the startup frame and keepalive ticks but NOT the forwarded body; omitting correlationId is a true no-op - tests/unit/attempt-logging-early-keepalive-merge.test.ts (new): real temp-DB end-to-end proof against the actual persisted call-log row — early bytes prepended in send order, consumed exactly once, no-op without a correlationId, gated by detailedLoggingEnabled matching the existing streamChunks capture gate - tests/unit/chatcore-attempt-logging.test.ts (existing): unchanged, still passing — confirms the merge addition doesn't disturb existing persistence behavior - 44 passed total across the above plus earlyStreamKeepalive.test.ts, 2 pre-existing skips unrelated to this change - tsgo --noEmit: clean on all touched files |
||
|
|
acd740908f | feat(providers): refresh Qwen3.8 model catalogs (#10226) | ||
|
|
7d92aa7527 | fix(streaming): preserve completed Codex tool handoffs (#10608) | ||
|
|
735d2c9659 |
fix(api): accept .opus uploads on /v1/audio/transcriptions (#10607)
Whisper-compatible upstreams pick the decoder from the multipart filename against an allow-list (flac, m4a, mp3, mp4, mpeg, mpga, oga, ogg, wav, webm) that has no `opus`, and OmniRoute forwarded the client's filename verbatim. The same bytes transcribed as `note.ogg` and 400'd as `note.opus`. Since /v1/audio/speech emits audio/opus for `response_format=opus`, clients re-uploading their own voice notes hit this on every round trip. A `.opus` file is Opus in an Ogg container (RFC 7845), so `.ogg` is a truthful relabel and is already on the allow-list. Rewrite the extension in getUploadedFileName, the single choke point feeding buildMultipartBody. The OpenRouter STT path had the same root cause with a quieter symptom: `.opus` matched neither its extension list nor its MIME map, so it fell through to the "wav" default and announced Opus bytes as WAV. Map both the extension and audio/opus to its already-supported ogg container. Fixes #10588 |
||
|
|
70f94685e6 | fix(gemini): inject missing items schema for array typed mcp tools (#10578) (#10605) | ||
|
|
a7b96b44e9 |
fix(xai): cap chat history at xAI 800-message limit (#10601)
* fix(xai): cap chat history at xAI 800-message limit xAI returns 413 when messages/input exceed 800 items. Token compression never fires on a long tool loop that still fits the context window, so trim at the executor edge after Responses expansion and drop orphaned tool pairs from the cut. * chore(changelog): attach PR number to xAI 800-message fragment * fix(xai): resolve TS2339 generic assignment in capXaiRequestHistory Drop the T extends Record<string, unknown> generic on capXaiRequestHistory and type it directly as Record<string, unknown> -> Record<string, unknown>. Assigning next.messages / next.input onto a generic T was rejected by TypeScript even though every call site already passes/consumes a JsonRecord (= Record<string, unknown>), so no caller relied on the generic preserving a narrower type. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: mikolaj92 <mikolaj92@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
6003612000 |
fix(audio): fall back nested STT models when the prefix provider has no credentials (#10584)
* fix(audio): fall back nested STT models when the prefix provider has no credentials Bare ids such as deepgram/nova-3 prefix-match the native provider and 400 when that key is missing, even if OpenRouter lists the same model. Retry the gateway and mention qualified catalog ids in the error. Closes #10583 * test(audio): scope whisper-1 fallback test to a 2-provider registry nanogpt was added to AUDIO_TRANSCRIPTION_PROVIDERS (already merged, unrelated to this fix) with a bare "whisper-1" model id, which now intercepts findAlternateAudioProvider's first candidate before the qualified-alias branch this test exists to cover. Scope the test to a local {openai, openrouter} registry subset so it deterministically exercises the qualified `${provider}/${model}` fallback regardless of future providers that also list a bare "whisper-1" id. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
3d0ffb49a4 |
feat(providers): complete Jina + Gemini Embedding 2 multimodal via OmniRoute (#10581)
* feat(providers): complete Jina AI via OmniRoute including Omni multimodal
Dashboard and env keys share one Jina credential pool, native v5 Omni
{text}/{image}/{content} docs pass through /v1/embeddings intact, and
classify/segment/search are proxied without a third unused Jina card.
* chore(changelog): name Jina complete-provider fragment for #10581
* feat(providers): make Gemini Embedding 2 multimodal work via OmniRoute
Route gemini-embedding-2 through embedContent/batchEmbedContents so N
OpenAI input items become N vectors, pass through native multimodal
parts, and use dashboard Gemini keys (GEMINI_API_KEY only as fallback).
* fix(providers): resolve rebase fallout for Jina/Gemini embeddings
- narrow the two new no-explicit-any violations introduced by this PR
(validateJinaFoundationProvider's params + catch, search.ts's
normalizeJinaSearchResponse data param)
- cast credentials to Record<string, unknown> at the two quota-preflight
call sites in src/sse/services/auth.ts so the new JinaEnvCredentials /
GeminiEnvCredentials union members type-check without loosening the
allRateLimited narrowing used elsewhere in the same function
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
|
||
|
|
228ef6fba9 |
fix(mcp): make GitHub skill tools discoverable through omniroute_tool_search (#10575)
Add githubSkillTools to getAllToolDefinitions() so the searchable MCP catalog matches TOTAL_MCP_TOOL_COUNT, which already counts them. The GitHub skill tools were registered and counted but missing from the catalog, so omniroute_tool_search could not surface them. Adds regression tests at both layers: catalog aggregation and client-visible discovery via the MCP client. Co-authored-by: Brandon Bennett <brandonbennett@macbookair.myfiosgateway.com> |
||
|
|
9222528bdd |
fix(opencode): session stability, free-tier routing, and CLI defaults (#10571)
* fix(opencode): session stability, free-tier routing, and CLI defaults - Wire generateSessionId() into opencodeHeaders so x-opencode-session is a deterministic fingerprint instead of randomUUID() per request, enabling upstream prompt caching across a conversation - Thread request body through buildHeaders() so session fingerprint has access to model, system, messages, and tools - Default CLI header synthesis to ON (opt-out via false), align values with 9router proven defaults (opencode/desktop/global) - Auto-echo listing-valid model names for noAuth providers so response.model matches /v1/models listing - Short-circuit free-tier model resolution to opencode provider first to prevent prefix inference misrouting when catalog is unreachable * fix(opencode): make free-tier default flip self-consistent + add coverage PR #10571 flipped OPENCODE_SYNTHESIZE_CLI_HEADERS to on-by-default and changed the synthesized UA/client/project default values, but shipped with 2 broken assertions in the existing #5997 regression test and no coverage for the new session-fingerprinting, free-tier routing, or noAuth echoModel logic (Hard Rule #18). - Update tests/unit/opencode-cli-headers-synthesis-5997.test.ts to match the new on-by-default behavior and new default values; add an explicit opt-out coverage test so the forward-only path is still guarded. - Fix 20 further test failures in tests/unit/opencode-executor.test.ts and tests/unit/refactor-buildHeaders-opencode.test.ts caused by the same default flip (pin OPENCODE_SYNTHESIZE_CLI_HEADERS=false for the characterization suites that predate #10571; use a genuinely CLI-looking UA where the preserved-UA test requires one). - Fix a real bug found via TDD while adding the mandated free-tier routing regression test: the big-pickle/*-free short-circuit in open-sse/services/model.ts checked activeProviders?.has("opencode") literally, but getActiveProviderSet() canonicalizes every connection's provider id through resolveProviderAlias(), which rewrites "opencode" to "opencode-zen" via a manual override — so an active no-auth opencode connection could never satisfy the check. Now checks both opencode-family candidate ids. Proven with a test that fails on the original code and passes with the fix (both connections active with a stale synced catalog omitting big-pickle). - Extract the noAuth-provider echoModel aliasing in chatCore.ts into a pure, directly-testable helper (open-sse/handlers/chatCore/noAuthEchoModel.ts), matching the existing chatCore god-file decomposition pattern. - Add regression tests for generateSessionId()-based x-opencode-session fingerprinting (stable within a conversation, changes on model/message changes), the free-tier routing short-circuit, and the noAuth echoModel aliasing. - Add the changelog.d/ fragment and sync docs/reference/ENVIRONMENT.md's OPENCODE_SYNTHESIZE_CLI_HEADERS/OPENCODE_USER_AGENT/OPENCODE_CLIENT/ OPENCODE_PROJECT rows to the new defaults. Does NOT resolve whether flipping OPENCODE_SYNTHESIZE_CLI_HEADERS's default was the right call, and does NOT touch the separate open PR #10357 which flips the same flag with a different literal default value - that decision is left to the maintainer at merge time. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
947a7c64d4 |
fix(providers): catalog OpenRouter Gemini Embedding 2 ids (#10566)
GET /v1/models listed google/gemini-embedding-001 but omitted google/gemini-embedding-2 even though that id already returns 3072-d vectors. Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
458ab1aac0 |
fix(vision): preserve high detail for inline images (#10554)
* fix(vision): preserve high detail for inline images * fix(vision): scope high-detail image default to OpenCode clients defaultImageDetail() was applied at prepareUpstreamBody, the shared upstream-body prep path for every provider and format, not just the OpenCode path the fix targets. Gate it on isOpencodeClient (the existing User-Agent/x-opencode-* header signal already used for bypassDefaultToolLimit at this call site) so non-OpenCode callers keep the provider's own image detail default. Adds a regression test covering a non-OpenCode caller against the same opencode-zen provider. * fix(vision): document and test the global vs OpenCode-only detail scope The OpenCode-only high-detail default in chatCore/upstreamBody.ts (defaultImageDetail, gated on isOpencodeClient) forwards the caller's own image_url.detail and was already correctly scoped in a prior commit on this branch. The internal vision-bridge describe self-loop (visionBridgeHelpers.ts) is architecturally global: VisionBridgeGuardrail runs for every caller/provider whenever the target model lacks vision support, and there is no client-identity signal at that layer to gate on. Its describe prompt explicitly asks the vision model to transcribe visible text, so requesting "high" detail unconditionally is justified on its own merits (OCR accuracy), independent of the OpenCode motivation. Adds a compatibility assertion proving the Anthropic wire-format branch of the same describe self-loop carries no `detail` field (it has no such concept) and is therefore unaffected by this default, and documents the split (OpenCode-only forwarding vs. global describe default) in docs/security/GUARDRAILS.md. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: rinseaid <rinseaid@rinseaid.net> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
b9cd5ed138 |
feat(providers): optional AI Horde API key and live image catalog (#10542)
* feat(providers): optional AI Horde API key and live image catalog Allow a registered Horde key on the no-auth connection and send it for chat and image jobs. List only image models that currently have workers, and generate through Horde's native async API. # Conflicts: # open-sse/config/imageRegistry.ts # src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx # src/shared/constants/providers.ts # src/sse/services/auth.ts * fix(providers): validate AI Horde keys against find_user The OpenAI-compatible /v1/models probe returns 200 for any Bearer token on oai.aihorde.net, so Check always succeeded. Use Horde's /v2/find_user lookup instead; an empty key still counts as the optional anonymous path. * chore(changelog): name the AI Horde fragment for #10542 * fix(images): harden AI Horde optional-key selection and outbound fetches - Optional-key selection now honors connection health (rate-limit cooldown and terminal/unavailable test status) before handing a stored key back, rotating to the next healthy key or falling back to the anonymous no-auth path instead of using an unhealthy stored key. - Route the Horde submit/check/status/cancel and catalog calls through the repository's bounded outbound-fetch helper (timeout, no more bare fetch()) and route R2 image downloads through the established bounded remote-image fetch (SSRF host guard, DNS-rebinding pin, streaming byte cap, redirect limit) instead of an unbounded fetch(). - Extend the generation deadline to cover the full request lifecycle (catalog freshness check, submit, polling, and image download), and add a regression test proving that exceeding the deadline issues a DELETE cancel to Horde's API rather than only timing out locally. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: pqr <pqr@soraka.ititti.es> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
f92075bb63 |
fix(deepseek): align V4 reasoning efforts across DeepSeek and OpenCode Go (#10540)
* fix(deepseek): align V4 reasoning efforts * docs(changelog): note DeepSeek effort fix * fix(deepseek): align OpenCode V4 effort aliases * test(deepseek): align effort alias expectation * fix(deepseek): scope low effort to v4 * fix(opencode-go): route DeepSeek V4 through Responses |
||
|
|
9a433775e7 |
fix(models): correct Codex context and combo limit resolution (#10533)
* fix(models): honor Codex combo context overrides * test(codex): align discovery context expectation * test(models): align Codex route limits * test(models): align remaining Codex route limits |
||
|
|
ebf0bf913a |
fix(settings,auth): default debugMode to false and skip account rotation on model-unsupported 400 (#10525)
* fix(settings,auth): default debugMode to false and skip account rotation on model-unsupported 400 * fix(auth): disambiguate model-unsupported from auth-credential 400 The model-unsupported guard used MODEL_ACCESS_DENIED_PATTERNS directly, which also matches auth-credential errors like 'invalid api key for model X'. Add the AUTH_CREDENTIAL_ERROR_PATTERNS exclusion (same as checkFallbackError) and use provider_model_unsupported log reason. Addresses maintainer feedback on PR #10525 Signed-off-by: Minxi Hou <houminxi@gmail.com> * fix(auth): narrow model-unsupported guard to avoid misclassifying account-scoped entitlement 400s The #10460 guard reused MODEL_ACCESS_DENIED_PATTERNS directly, which also matches ambiguous "access"/"permission" phrasing (e.g. "does not have permission to access this model") that commonly signals an ACCOUNT-scoped entitlement gap (PRO vs free tier) rather than a genuinely provider-wide unsupported model — a different account of the same provider may still have access, so those must keep rotating normally instead of being short-circuited. Extract isProviderModelUnsupported400() in accountFallback.ts: reuses the same AUTH_CREDENTIAL_ERROR_PATTERNS exclusion checkFallbackError's 400 branch already applies, narrowed to a strict subset of unambiguous "provider does not serve this model at all" phrasings. auth.ts now calls this shared helper instead of testing the broader patterns in isolation, and exposes the sanitized reason ("provider_model_unsupported") on the returned result, not just in the log line. Also fix DATA_DIR test-isolation ordering in account-fallback-service.test.ts: it was assigned after the first dynamic import of accountFallback.ts, which transitively imports src/lib/db/core.ts (DATA_DIR is captured once at module-load time), so the intended isolated test directory was silently never used. Move the assignment before any transitive DB import, and add regression tests for the 3-account rotation contract: exactly one upstream call for an unambiguous provider-wide 400 with the combo advancing to the next target, continued rotation for account-scoped 401/403/429 and for the permission/entitlement 400 case that motivated this narrowing. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Signed-off-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
9500adb013 |
fix(combo): surface context-overflow before compression so oversized requests fail fast with a clear error (#10225) (#10503)
* fix(combo): surface context-overflow before compression so oversized requests fail fast with a clear error (#10225) * fix(combo): make context-overflow deferral target-aware for native Codex passthrough (#10225) The deferral added by the prior commit checked only operator-named compression exclusions when deciding whether at least one target "can compress" — it never accounted for native Codex Responses passthrough targets, which chatCore.ts unconditionally excludes from compression (compressionExcluded = nativeCodexPassthrough || ...). Deferring on such a target's account let an oversized request skip both the combo preflight AND compression, reaching fetch() uncompressed. Thread the same request-shape facts chatCore.ts uses (shouldUseNativeCodexPassthrough: provider/sourceFormat/endpointPath/body/ headers) down into getKnownContextOverflow so the deferral decision can never drift from chatCore's own — a native-codex-passthrough target now never counts as "compressible", so a pool made only of such targets keeps the fast local 400 instead of a wasted round trip. Adds regression coverage: the pure getKnownContextOverflow target-aware check, an end-to-end handleComboChat proof that a native-codex-only pool fails fast with zero dispatches, and two real handleChatCore-path tests proving compression actually reduces the dispatched body when eligible, and that a still-too-large-after-compression request is rejected locally without an upstream call. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
2230fbbe93 |
fix(resilience): keep combo quality and auth reasons separate and redact connection labels in terminal errors (#10314) (#10501)
* fix(resilience): keep combo quality and auth reasons separate and redact connection labels in terminal errors (#10314) * fix(resilience): sanitize identifiers in error text, add explicit terminal-status policy, fix classifier ordering (#10314) Four gaps in the prior combo-error-aggregation fix: - formatComboOutcomes() only redacted connection identifiers in the model label, never in the raw upstream error TEXT — a proxy echoing a connection/account id back in its error body leaked it into the client-facing terminal message. Redact both. - The terminal HTTP status was still `lastStatus` — whichever target happened to fail last, independent of the other targets' reasons. Add resolveComboTerminalStatus(): preserve a 4xx only when every eligible target's failure is genuinely "the request is invalid" (model-class); a heterogeneous mix (e.g. a quality failure + a sibling's 401) now normalizes to a 5xx-class status reflecting an infra/provider problem, never a misleading client error borrowed from an unrelated target. - classifyComboOutcome()'s ordering had `status === 408 || status >= 499` checked before `status >= 500`, making the provider branch permanently unreachable — every real 5xx (500/502/503/504) was silently mislabeled as "timeout". Fixed to an exact match (408/499) and gave 429 its own explicit `rate_limit` kind instead of falling into the generic "model" (request-invalid) bucket by accident. - Added an integration-level regression driving the real handleComboChat wiring end-to-end (quality failure + sibling 401, and a success-after- quality-failure case), not just the pure aggregation helpers. Updated three pre-existing tests whose assertions encoded the OLD last-writer-wins contract this fix intentionally supersedes (#8486 Part B antigravity retryAfter tests, two combo-routing-engine status/message tests) to the new, more precise contract; verified the underlying #8486 concern (wrong target's retryAfter header) is still honored under the new status policy. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
da42ed6d2e |
fix(providers): fall back to public Code Suggestions endpoint on GitLab Duo direct_access 401 (#10365) (#10499)
* fix(providers): fall back to public Code Suggestions endpoint on GitLab Duo direct_access 401 (#10365) * fix(providers): extend GitLab Duo 401 fallback to the connection-test path (#10365) The chat-completion path (open-sse/executors/gitlab.ts) already falls back to the public Code Suggestions completions endpoint when the direct_access exchange is rejected with 401, but testOAuthConnection() / the dashboard Retest button still reported the connection unhealthy on the same 401 — even though a real chat request through that connection would have succeeded via the fallback. Apply the identical fallback contract to the connection-test path (first attempt and the post-refresh retry), sharing the predicate with the executor via shouldFallbackToPublicCodeSuggestions. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
15386495c2 |
fix(compression): add i18n support for less-code and terse-prose (#10498)
* fix(compression): add i18n support for less-code and terse-prose Translates less-code output style to pt-BR, vi, ja, and id. Adds missing vi translation to terse-prose caveman mode. Removes less-code from English-only allowlist and updates matrix tests. Fixes #10426 * docs(compression): add output styles coverage table Adds the requested Output Styles matrix to the compression guide covering styles, supported languages, and intensity levels. Fixes #10426 |
||
|
|
50da54484e |
fix: downgrade adaptive thinking and gate context-1m beta on model eligibility (#10119) (#10481)
* fix(sse): downgrade adaptive thinking and gate context-1m beta on model eligibility (#10119) * fix(sse): thread resolved model into DefaultExecutor's anthropic-beta merge (#10119) DefaultExecutor.buildHeaders() merged the client-negotiated anthropic-beta header without ever passing the resolved target model into mergeClientAnthropicBeta(), so the context-1m-2025-08-07 eligibility gate added earlier in this PR could not see which model a combo/fallback had actually routed to at this call site. buildHeaders() now accepts an optional model parameter (mirroring BaseExecutor.buildHeaders' existing signature and the pattern already used by grok-cli.ts/qoder.ts) and forwards it through, so an ineligible model target (e.g. Haiku) has the beta dropped instead of forwarded blind. Restores a CHANGELOG bullet (PR #10366) that a prior merge auto-resolve had dropped from this branch. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
7a6fcfc74d |
fix: resolve adaptive latency-collapse self-lock with solo-progress and idle recovery (#10111) (#10478)
* fix(admission): resolve adaptive latency-collapse self-lock with solo-progress and idle recovery (#10111) * fix(admission): refresh recovery ceiling on updateConfig (#10111) updateConfig() clamped currentLimit to the new min/maxLimit but left recoveryCeiling pinned to the value computed at construction time, so a raised initialLimit could never recover past the stale ceiling and a lowered one could leave the ceiling above the new maxLimit. Recompute recoveryCeiling from the new initialLimit on every updateConfig call, clamped to the (possibly also new) min/maxLimit. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
97e504cdbf |
fix(sse): stop leaking upstream control lines to OpenAI-format clients (#10017) (#10473)
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
e667ab12d1 |
fix(usage): wire agentrouter balance quota into dashboard Quota UI (#10078) (#10472)
* fix(usage): wire agentrouter balance quota into dashboard Quota UI (#10078) * fix(usage): render AgentRouter wallet balance as USD in the Quota UI (#10078) The prior fix wired AgentRouter's balance into getUsageForProvider() and USAGE_SUPPORTED_PROVIDERS, but the actual dollar figure never reached the Dashboard Quota UI: quotas.balance.remaining carried a synthetic two-state percent (100/0) instead of the real dollarBalance, and the Provider Limits renderer only formats a row as "$X.XX" when isCredits/currency/creditCount are set, which the generic quota-parsing path never sets. A configured balance rendered as a bare "100% left" percentage, not USD. Shape quotas.balance.remaining as the real USD amount (clamped to 0) and add an agentrouter branch to quotaParsing.ts that builds a credits-style row (same buildCreditsQuota() pattern as DeepSeek/Claude extra-usage), so a configured balance shows a currency-formatted dollar amount and an exhausted balance always renders as exactly $0.00. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
09680013de |
fix(providers): resolve combo names on /v1/audio/speech and /v1/videos/generations (#10471)
* fix(providers): resolve combo names on /v1/audio/speech and /v1/videos/generations `GET /v1/models` advertises combos with `owned_by: combo`, and chat, embeddings, transcriptions (#9134) and images (#8986, #9239) all resolve those names. Speech and video did not: both rejected a combo name at model validation, before any resolution could happen. POST /v1/audio/speech {"model":"my-combo","input":"hi"} -> 400 Invalid speech model: my-combo. Use format: provider/model POST /v1/videos/generations {"model":"my-combo","prompt":"a cube"} -> 400 Invalid video model: my-combo. Use format: provider/model A client picking a model out of /v1/models therefore could not tell which entries the catalogue would actually accept, and callers ended up hardcoding vendor ids for these two routes while using combo names everywhere else. Both routes now mirror the images route: detect a combo name before the provider lookup and divert to a strategy executor. The two new executors follow imageCombo — expand targets with resolveComboTargets(), filter to targets the route can actually serve, walk them in priority order, and return the first success or the last failure, with 400/401/403 treated as terminal. Two details differ from the image strategy: Speech filters at model level rather than provider level. parseSpeechModel() resolves a provider prefix without checking that the model behind it can speak, so `openai/gpt-4o` would otherwise be accepted as a target and fail only once dispatched. The filter now checks the provider's own model list, and keeps targets from dynamic provider nodes that do not enumerate models. Speech also returns the handler's Response untouched instead of building a JSON body, because that route streams audio; only the ADD-only meta headers are attached, exactly as the direct path does. The failure branch is the only place the body is read. successfulMediaGenerationResponse() gains optional `strategy` and `fallbackAttempts` so the video strategy can report them the way imageCombo does, rather than duplicating the cost calculation. Both are omitted on the direct single-model path, where neither is meaningful. Tests mirror tests/unit/combo/image-combo.test.ts for both routes: combo not found, no capable targets, empty combo, and targets present with no provider connection. 16/16 pass across the three combo test files. * fix(providers): preserve local overrides, custom models and per-target prompt rules through video combo dispatch executeVideoCombo() diverged from the direct /v1/videos/generations route in three ways: it dropped the ComfyUI-style local-override credential lookup for authType:"none" targets, its capability filter only matched the built-in video registry (skipping custom OpenAI-compatible provider nodes tagged with the "videos" endpoint), and the route validated the prompt against the unresolved combo name before combo targets were expanded — rejecting prompt-optional I2V targets that never got the chance to opt out. Extracts the shared resolution rules (resolveVideoModelTarget, isVideoPromptOptional, resolveLocalOverrideCredentials) into src/app/api/v1/_shared/videoModelResolution.ts so the direct route and the combo executor apply identical rules, moves the combo-name diversion ahead of the prompt-required check so validation runs against the real resolved target, and adds per-target prompt validation inside the combo loop so a missing prompt only rules out that target instead of the whole combo. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
7f0404bf82 |
fix(open-sse): stop concurrent requests colliding on dedup hash for non-OpenAI formats (#10438)
* fix(open-sse): stop concurrent requests colliding on dedup hash for non-OpenAI formats computeRequestHash() in requestDedup.ts projected the prompt content from body.messages only. The dedup site in chatCore.ts hashes the *translated* (target-format) request body, and non-OpenAI target formats don't carry a messages field: Gemini-translated bodies use `contents`, Responses-API bodies use `input`. So for those formats messages was always undefined, every prompt hashed to the same null-backed value for a given model, and concurrent requests with different prompts joined the same in-flight promise -- the second caller silently received the first caller's response verbatim (#10249). Fix: project body.messages ?? body.contents ?? body.input ?? null instead of only body.messages, keeping the rest of the canonical hash projection unchanged. Genuinely identical concurrent requests still dedupe (the intended perf behavior); different prompts under Gemini/Responses-API target formats no longer collide. Regression test: tests/unit/request-dedup-10249.test.ts reproduces the two collision scenarios from the plan-file (Gemini `contents`, Responses-API `input`), confirms the OpenAI `messages` case was already correct, and asserts identical-request dedup keeps working. Verified RED (byte-identical hashes 0b24fd88.../dc16d5b7... pre-fix) -> GREEN (distinct hashes, dedup preserved) against this exact diff. * fix(open-sse): cover nested translator shapes + system fields in dedup hash (#10438) computeRequestHash() only read top-level body.messages ?? body.contents ?? body.input, but several translated request shapes nest their prompt content: the Antigravity Cloud Code envelope under request.contents, and Kiro under conversationState.currentMessage.userInputMessage.content (plus conversationState.history). Two different concurrent prompts to those targets could hash identically and share/leak a response between callers. Adds extractPromptContent()/extractSystemContent() helpers covering every prompt-bearing shape produced by open-sse/translator/request/*.ts (OpenAI/Cursor messages, Claude messages+system, Gemini contents+ systemInstruction, Responses input+instructions, Antigravity and Kiro nesting), and folds system/instructions/systemInstruction into the canonical hash so two requests with the same user message but a different system prompt no longer collide either. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> |
||
|
|
5240afed42 |
fix(antigravity): strip trailing model turn for native Gemini requests too (#10436)
* fix(antigravity): strip trailing model turn for native Gemini requests too Newer Gemini endpoints reject a request ending on a model turn with HTTP 400 'Requests ending with a model turn are not supported' — the same rejection class Claude hits via Vertex. transformRequest() previously wired stripTrailingAntigravityAssistantTurn() only into the isClaude branch, so native Gemini models routed through Antigravity kept a trailing role:model entry and hit the 400. Extend the guarded strip (never empties contents) to native Gemini models too, gated by upstreamModel including "gemini". The Claude path is untouched (byte-identical), preserving PR #6114's live validation against Vertex Claude. Flips tests/unit/antigravity-claude-prefill-strip.test.ts test (b), which previously asserted the buggy pass-through, and adds (b2) for the gemini-3-flash-agent tier. Closes #10104 * fix(antigravity): scope Gemini trailing-turn workaround Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: adevwithpurpose <adevwithpurpose@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> |
||
|
|
231b16ef18 |
fix(auto): rate-limit empty-pool AUTO warnings (#10344)
Family resolves like auto/zai with no connected models logged a warn on every call (about once a minute per poll). Keep the empty-pool behavior; emit the warn at most once per label per 60s. Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> |
||
|
|
ceced68817 |
feat(oauth): add gemini-3.7-flash models for antigravity and agy providers (#10305)
* feat(oauth): add gemini-3.7-flash models with reasoning tiers for antigravity Support gemini-3.7-flash and its thinking tiers (low/medium/high) for antigravity and agy providers. - Define public models, pricing, modelSpecs, and CLI tool definitions - Map tiers to live upstream id gemini-3.7-flash-tiered - Configure defaultThinkingBudget (low: 1024, medium: 8192, high: 32768) - Allow executor fallback on upstream 404 and 5xx errors - Add unit tests in antigravity-model-aliases.test.ts * fix(oauth): expose gemini-3.7-flash as one callable antigravity/agy model Upstream (fetchAvailableModels on daily-cloudcode-pa) only accepts the single upstream id gemini-3.7-flash-tiered; the high/medium/low suffixed tier ids 404. Registering all four as distinct public model ids violates the base #3696 uniqueness invariant (no two ANTIGRAVITY_PUBLIC_MODELS entries may resolve to the same upstream id). Collapse to the single live gemini-3.7-flash public model (aliased to gemini-3.7-flash-tiered) and drop the tiered specs, pricing, free-catalog and CLI entries accordingly, keeping the leading public model order (Gemini 3.6 tiers first) intact. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> Co-authored-by: Chewji9875 <Chewji9875@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
1089c24bc8 |
Remove/mimocode sunset provider (#10186)
* remove: drop sunset MiMoCode provider from model catalog * remove: drop sunset MiMoCode provider from model catalog (shared.ts) Remove unused imports, types, and comments from shared.ts. * remove: MiMoCode provider (Xiaomi sunset) — executor, registry, no-auth config, icon, tests * refactor(providers): finish MiMoCode removal — sweep remaining no-auth references Drop the leftover mimocode entries from the no-auth provider controls, the translate-path snapshot, the eslint suppressions, and the #3061 auth-loop test. Re-point the fingerprint-pin (#6696) and proxy-noauth (#6272) tests at opencode, which exercises the same fingerprint path, so the removal does not break runtime behavior. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * docs(providers): reconcile provider/executor counts after MiMoCode sunset The base's parallel doc-count sync (#10433) pinned 340 providers / 101 executors. With mimocode removed, live code has 339 providers and 100 executors; refresh the user-facing counts (package.json description, llm.txt, README/AGENTS, i18n llm.txt, provider reference, diagrams) so the check-docs-counts STRICT gate stays green. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * test(providers): fix orphaned mimocode references after MiMoCode sunset The sunset removed mimocode/mcode from the free-onboarding candidates and from FINGERPRINT_PROVIDERS, but two tests still referenced them: - free-provider-onboarding-setup: the mimocode->theoldllm substitution introduced duplicate 'opencode' rows (impossible given the request-set dedupe) and the wrong display name; align expectations with the actual {opencode, theoldllm} dedupe behavior and 'The Old LLM (Free)' name. - combo-system-prompt-templates-5501: resolveTargetFingerprint tested with provider 'mcode', which is no longer a fingerprint provider; point it at the remaining fingerprint provider 'opencode'. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: Tushar49 <Tushar49@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> |