From f2e36ad0ce730b710036a3002bc35cd0f93f5333 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Thu, 6 Aug 2026 10:40:56 -0300 Subject: [PATCH] fix(ci): clear base-reds on release/v3.8.50 (migration collision + 4 masked gates) (#9600) Validated in local merge-train (diegosouzapw batch) --- config/quality/file-size-baseline.json | 11 +- config/quality/quality-baseline.json | 3 +- open-sse/handlers/chatCore.ts | 1 + .../services/__tests__/tierResolver.test.ts | 11 +- open-sse/services/combo/dispatchPrelude.ts | 41 +++- scripts/build/pack-artifact-policy.ts | 3 + skills/omni-version-manager/SKILL.md | 226 ++++++++++++++++++ src/i18n/messages/en.json | 5 +- src/i18n/messages/pt-BR.json | 12 +- src/i18n/messages/vi.json | 12 +- src/lib/db/migrationRunner.ts | 7 + ...opted.sql => 137_auto_restart_adopted.sql} | 2 +- ...end.sql => 138_dario_fallback_backend.sql} | 2 +- src/lib/services/installers/dario.ts | 13 - tests/unit/feature-flags-settings.test.ts | 6 +- tests/unit/providers-constants-split.test.ts | 14 +- tests/unit/runtime-env.test.ts | 3 + tests/unit/sidebar-costs-section.test.ts | 6 +- ...tls-client-node-docker-binary-7802.test.ts | 5 +- 19 files changed, 330 insertions(+), 53 deletions(-) rename src/lib/db/migrations/{135_auto_restart_adopted.sql => 137_auto_restart_adopted.sql} (93%) rename src/lib/db/migrations/{136_dario_fallback_backend.sql => 138_dario_fallback_backend.sql} (91%) diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 0dd2f9b976..b0bcf0cc8c 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -524,7 +524,7 @@ "_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).", "_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.", "open-sse/executors/antigravity.ts": 1528, - "open-sse/executors/base.ts": 1635, + "open-sse/executors/base.ts": 1640, "open-sse/executors/chatgpt-web.ts": 3241, "open-sse/executors/codex.ts": 1562, "open-sse/executors/cursor.ts": 1560, @@ -539,7 +539,7 @@ "open-sse/mcp-server/schemas/tools.ts": 1505, "open-sse/mcp-server/server.ts": 1411, "open-sse/mcp-server/tools/advancedTools.ts": 1120, - "open-sse/services/accountFallback.ts": 1966, + "open-sse/services/accountFallback.ts": 1972, "open-sse/services/adobeFireflyClient.ts": 2385, "open-sse/services/claudeCodeCompatible.ts": 1202, "open-sse/services/combo.ts": 3648, @@ -558,7 +558,7 @@ "src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2615, "src/app/(dashboard)/dashboard/health/page.tsx": 1165, "src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1316, - "src/app/(dashboard)/dashboard/providers/page.tsx": 1928, + "src/app/(dashboard)/dashboard/providers/page.tsx": 1944, "src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1201, "src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1019, "src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1464, @@ -573,7 +573,7 @@ "src/lib/tokenHealthCheck.ts": 1021, "src/lib/db/apiKeys.ts": 1529, "src/lib/db/core.ts": 1637, - "src/lib/db/migrationRunner.ts": 1077, + "src/lib/db/migrationRunner.ts": 1084, "src/lib/db/models.ts": 1097, "src/lib/db/providers.ts": 1034, "src/lib/memory/retrieval.ts": 1073, @@ -605,5 +605,6 @@ "_rebaseline_2026_08_02_v3850_agentrouter_responses": "Release v3.8.50 AgentRouter/Codex compatibility reconciliation. open-sse/executors/base.ts 1562->1578: #9190 wires AgentRouter's selected Claude/OpenAI/Responses protocol through the existing executor URL, auth, identity-header and fingerprint chokepoints; the reusable alternate resolver remains outside base.ts. open-sse/utils/stream.ts 2887->2889: #9213 evaluates Responses ID and usage normalization independently so response.completed always receives finite usage.total_tokens instead of short-circuiting after an ID rewrite. tests/unit/chatcore-translation-paths.test.ts 2769->2776: #9191 updates the existing Claude-Code bridge assertions for the dynamic AgentRouter wire image. PR #9224 offsets its own chatCore growth by extracting the AgentRouter protocol decisions into chatCore/agentRouterProtocol.ts, leaving chatCore below its frozen ceiling. Covered by agentrouter executor/chatCore protocol tests, chatcore translation-path tests, and responses-commentary-passthrough tests.", "_rebaseline_2026_08_05_9323_agentrouter_waf_retry": "PR #9323 (fix(agentrouter): retry on 400 content-blocked + burst guard) own growth: open-sse/executors/base.ts 1578->1623 (check-file-size.mjs conta via split(\"\\n\").length; wc -l ve 1622). As +45 linhas sao o WAF_RETRY_CONFIG + o burst guard via gateOutboundRequest() para o WAF do agentrouter.org, com comentarios explicando o porque de cada mitigacao e cobertos por tests/unit/base-executor-waf-retry.test.ts e tests/unit/wafRateLimit.test.ts. Crescimento funcional legitimo, nao inchaco.", "_rebaseline_2026_08_05_9529_own_growth": "PR #9529 own growth (base release/v3.8.50 medida EXATAMENTE nos frozen antigos, entao o modo base-relative #8522 nao cobre): open-sse/services/rateLimitManager.ts 1060->1105 (+45: helper applyLimiterSettings() que re-arma o heartbeat do reservoir apos updateSettings — fix do bug Bottleneck 2.19.5 que congelava a fila weighted; TDD em tests/unit/ratelimit-reservoir-refresh.test.ts); tests/integration/chat-pipeline.test.ts 1592->1598 (+6: User-Agent do codex derivado de getCodexClientVersion() em vez de literal pinado — teste-irmao alinhado ao contrato); tests/unit/provider-validation-specialty.test.ts 2980->2985 (+5: cobertura NOVA claude-web 429 -> valid:false, alinhamento #9406); open-sse/translator/response/openai-responses.ts 1174->1204 (+30: buildResponsesReasoningSummaryDelta MOVIDA do leaf pureHelpers.ts para o host — a funcao do #9500 muta stream state e violava o contrato do leaf puro; o LOC total do par host+leaf nao cresceu, o pureHelpers encolheu o mesmo tanto). Crescimento por fix de producao + cobertura adicional + realocacao arquitetural, nao inchaco.", - "_rebaseline_2026_08_06_v3850_inherited_drift_reconcile": "Reconciliacao 2026-08-06 do drift ACUMULADO da release/v3.8.50 apos o lote de merges de 08-05/06: 13 arquivos acima do frozen no tip puro 8180b49ce1 (medidos pelo proprio gate). O modo PR base-relative (#8522) deixa PRs inocentes passarem, e os rebaselines individuais dos PRs se perderam nas resolucoes sucessivas de conflito deste hot-file — o drift so aparece no modo absoluto (nightly/local). Crescimentos funcionais dos PRs mergeados: #9024 topology click-nav src/app/(dashboard)/dashboard/HomePageClient.tsx; #9324 OpenRouter enrich src/app/(dashboard)/dashboard/providers/page.tsx; #9329 quota card ordering src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx; #9193 context-window suffixes src/sse/handlers/chat.ts; #9332 nested Claude server tool ids open-sse/executors/base.ts; #9228 strip orphaned tool outputs open-sse/executors/codex.ts; #9236 nvidia tool-name normalize open-sse/executors/default.ts; #9314 nested tool_call validation open-sse/executors/kiro.ts; #9260 caller identity REST hops open-sse/mcp-server/server.ts; #8934 cache breakpoints tests tests/unit/chatcore-translation-paths.test.ts; #9193 suffix tests tests/unit/combo-routing-engine.test.ts; #9196 reasoning-on-tool-finish tests tests/unit/sse-auth.test.ts; #9163 GPT-5.6 Max reasoning tests tests/unit/translator-openai-to-kiro.test.ts. default.ts e kiro.ts entram no frozen (estavam sem entrada, acima do cap 1000). Atualizacao pos-medicao (a base avancou durante o ciclo do PR): src/sse/handlers/chat.ts 1857->1877 (#9184 affinity EOF evict) e open-sse/executors/default.ts 1027->1042 (#9005 Kimi K3 tool-name backfill)." + "_rebaseline_2026_08_06_v3850_inherited_drift_reconcile": "Reconciliacao 2026-08-06 do drift ACUMULADO da release/v3.8.50 apos o lote de merges de 08-05/06: 13 arquivos acima do frozen no tip puro 8180b49ce1 (medidos pelo proprio gate). O modo PR base-relative (#8522) deixa PRs inocentes passarem, e os rebaselines individuais dos PRs se perderam nas resolucoes sucessivas de conflito deste hot-file — o drift so aparece no modo absoluto (nightly/local). Crescimentos funcionais dos PRs mergeados: #9024 topology click-nav src/app/(dashboard)/dashboard/HomePageClient.tsx; #9324 OpenRouter enrich src/app/(dashboard)/dashboard/providers/page.tsx; #9329 quota card ordering src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx; #9193 context-window suffixes src/sse/handlers/chat.ts; #9332 nested Claude server tool ids open-sse/executors/base.ts; #9228 strip orphaned tool outputs open-sse/executors/codex.ts; #9236 nvidia tool-name normalize open-sse/executors/default.ts; #9314 nested tool_call validation open-sse/executors/kiro.ts; #9260 caller identity REST hops open-sse/mcp-server/server.ts; #8934 cache breakpoints tests tests/unit/chatcore-translation-paths.test.ts; #9193 suffix tests tests/unit/combo-routing-engine.test.ts; #9196 reasoning-on-tool-finish tests tests/unit/sse-auth.test.ts; #9163 GPT-5.6 Max reasoning tests tests/unit/translator-openai-to-kiro.test.ts. default.ts e kiro.ts entram no frozen (estavam sem entrada, acima do cap 1000). Atualizacao pos-medicao (a base avancou durante o ciclo do PR): src/sse/handlers/chat.ts 1857->1877 (#9184 affinity EOF evict) e open-sse/executors/default.ts 1027->1042 (#9005 Kimi K3 tool-name backfill).", + "_rebaseline_2026_08_06b_v3850_sweepreds_drift": "Segunda reconciliacao de 2026-08-06 (/sweep-reds sobre o tip puro 2ddbbc61a6): 3 arquivos voltaram a passar do frozen apos os merges do mesmo dia, com atribuicao 1:1 por commit. (1) src/app/(dashboard)/dashboard/providers/page.tsx 1928->1944 e (2) open-sse/executors/base.ts 1635->1640, ambos do #9515 (feat(radar): flag-gated signed free-model catalog overlay, commit e7f6b1d130) — o overlay do Radar entra por wiring nos chokepoints ja existentes (a resolucao/verificacao do catalogo assinado mora fora destes dois arquivos); +16 e +5 linhas liquidas nao sao extraiveis sem inventar um leaf por callsite. (3) open-sse/services/accountFallback.ts 1966->1972 do #8704 (commit c4527f97bd), +6 linhas de dados em CREDITS_EXHAUSTED_SIGNALS ('has been exhausted', fixes #8631). src/sse/handlers/chat.ts 1880>1877 tambem estava violando e NAO entra aqui de proposito: e drenado por encolhimento na PR #9598, sem rebaseline. Crescimento proprio DESTA PR: src/lib/db/migrationRunner.ts 1077->1084 (+7) — o guard retroativo em isSchemaAlreadyApplied para os arquivos renumerados 137/138, exigido pela propria mensagem de erro de colisao do runner (ambas as migracoes sao ALTER TABLE ADD COLUMN puro, nao idempotente). Dois `case` + dois `return hasColumn(...)` + 3 linhas de comentario dentro do switch existente; nao extraivel." } diff --git a/config/quality/quality-baseline.json b/config/quality/quality-baseline.json index 864b57ca0a..784e3fe241 100644 --- a/config/quality/quality-baseline.json +++ b/config/quality/quality-baseline.json @@ -82,9 +82,10 @@ "tightenSlack": 10 }, "openapiCoverage.pct": { - "value": 38, + "value": 39.2, "direction": "up", "eps": 0.5, + "_tighten_2026_08_06_v3850_sweepreds": "38.0 -> 39.2 (aperto EXIGIDO pelo step 'Require-tighten (blocking)', que estava vermelho em ~60 PRs abertas de release/v3.8.50 — base-red herdado, nao defeito das PRs). A cobertura melhorou no ciclo porque as rotas novas entraram documentadas. 39.2 = valor medido pelo CI Quality Ratchet no run 31088889488; o tip puro 2ddbbc61a6 mede 39.3 localmente (npm run check:openapi-coverage: 247/628 rotas), entao 39.2 e o valor conservador dos dois. Aperto = gate mais ESTRITO, nunca mascaramento.", "_tighten_2026_07_04_v3844_release": "36.9 -> 39.3 (aperto exigido pelo --require-tighten no PR de release #5925). A cobertura OpenAPI melhorou no ciclo (9 rotas documentadas em 8fb020676 + as rotas novas de #5939/#5817/#6034/#5998 documentadas junto das features). 39.3 = valor medido pelo CI Quality Ratchet no run 28708141003 (tip 00c55afcb).", "_rebaseline_2026_06_28_v3839_release": "37.8 -> 36.9 (-0.9, beyond the 0.5 eps). v3.8.39 cycle drift surfaced ONLY on the release PR (the openapi-coverage ratchet does NOT run on PR->release fast-gates). The cycle added API/internal routes (antigravity paste-credentials onboarding, CCR ranged/grep/stats retrieve params, mcp 404 session handling) faster than docs/openapi.yaml coverage; documenting LOCAL_ONLY/internal onboarding routes in the PUBLIC spec would be gaming (same precedent as _rebaseline_2026_06_18_v3828_cycle_close). Measured by CI collect-metrics (run 28317145160) = 36.9. My release-finalize tree touches no routes (only the openapi.yaml version bump). Raising coverage by documenting public routes is tracked as follow-up doc debt.", "_rebaseline_2026_06_23_v3834_release": "38.4 -> 37.8 (-0.6, beyond the 0.5 eps so it failed the ratchet). v3.8.34 cycle drift: contributor PRs added API routes (e.g. quota/usage/opencode-go endpoints) faster than openapi.yaml coverage; the openapi-coverage ratchet does NOT run on PR->release fast-gates so it surfaced only on the release PR. Verified my release-finalize working tree touches no routes / openapi paths (only version bump in openapi.yaml). Measured by CI quality:collect (run 28000387577) = 37.8. Raising coverage by documenting the new routes is tracked as follow-up doc debt.", diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 9fc8da3a8c..373a0e6ea1 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -223,6 +223,7 @@ import { recordCost } from "@/domain/costRules"; import { calculateCost } from "@/lib/usage/costCalculator"; import { buildClaudePassthroughToolNameMap, + mergeResponseToolNameMap, normalizeOpenAIToolFinishReasons, restoreNonStreamingToolNames, } from "./chatCore/passthroughToolNames.ts"; diff --git a/open-sse/services/__tests__/tierResolver.test.ts b/open-sse/services/__tests__/tierResolver.test.ts index e713f037d8..06836772c3 100644 --- a/open-sse/services/__tests__/tierResolver.test.ts +++ b/open-sse/services/__tests__/tierResolver.test.ts @@ -192,12 +192,17 @@ describe("TierResolver", () => { }); it("uses cache for repeated models", () => { - classifyTiers([ + clearTierCache(); + const results = classifyTiers([ { provider: "openai", model: "gpt-4o" }, { provider: "openai", model: "gpt-4o" }, ]); - // If cache works, second call should be instant; test passes if no error - expect(true).toBe(true); + // Observable effect of the cache: the duplicate resolves to the same tier and only + // ONE entry is memoized (getTierStats counts cache entries, not classify calls). + assert.equal(results.length, 2); + assert.equal(results[0].tier, results[1].tier); + const stats = getTierStats(); + assert.equal(stats.free + stats.cheap + stats.premium, 1); }); }); diff --git a/open-sse/services/combo/dispatchPrelude.ts b/open-sse/services/combo/dispatchPrelude.ts index 9c30bf8233..833b8f86c4 100644 --- a/open-sse/services/combo/dispatchPrelude.ts +++ b/open-sse/services/combo/dispatchPrelude.ts @@ -21,6 +21,7 @@ import { handlePipelineChat, type PipelineStep } from "../pipeline.ts"; import type { resolveComboSetupConfig } from "../comboConfig.ts"; import { clampComboDepth, MAX_GLOBAL_ATTEMPTS, resolveDelayMs } from "./comboPredicates.ts"; import { resolveComboRuntimeUnits, resolveComboTargets } from "./comboStructure.ts"; +import { isComboModelVisible } from "./comboVisibility.ts"; import { buildFusionHandleSingleModel, extractFusionPanelSpec } from "./fusionPanel.ts"; import { clampStickyWeightedTargetLimit, @@ -340,12 +341,20 @@ export async function tryFusionDispatch(args: { runCombo: RunCombo; }): Promise { const { cfg, combo, config, strategy, log } = args; - const judgeModel = typeof cfg.judgeModel === "string" ? cfg.judgeModel : undefined; + const configuredJudge = typeof cfg.judgeModel === "string" ? cfg.judgeModel : undefined; + // The panel is filtered for hidden models by resolveComboTargets, but the + // explicit judge is a bare string that never passes through it (#8878). Drop a + // hidden judge so fusion falls back to a surviving panel member instead of + // dispatching a model the operator hid. + const judgeModel = + configuredJudge && !isComboModelVisible(configuredJudge, null, args.hiddenModelsByProvider) + ? undefined + : configuredJudge; const fusionTuning = cfg.fusionTuning && typeof cfg.fusionTuning === "object" ? (cfg.fusionTuning as FusionTuning) : undefined; - if (strategy !== "fusion" && (judgeModel || fusionTuning)) { + if (strategy !== "fusion" && (configuredJudge || fusionTuning)) { log.warn( "COMBO", `Combo "${combo.name}" sets config.judgeModel/fusionTuning but strategy is "${strategy}" — these fields are only consumed by the fusion strategy and will be ignored (#6455)` @@ -353,16 +362,30 @@ export async function tryFusionDispatch(args: { } if (strategy !== "fusion") return null; - const { panel: fusionModels, comboRefUnits } = extractFusionPanelSpec( - resolveComboTargets( - combo, - args.allCombos, - clampComboDepth(config.maxComboDepth), - args.hiddenModelsByProvider - ).map((target) => target.modelStr), + const resolvedFusionTargets = resolveComboTargets( + combo, + args.allCombos, + clampComboDepth(config.maxComboDepth), + args.hiddenModelsByProvider + ); + // extractFusionPanelSpec only understands model strings / combo refs, so the + // resolved targets have to be flattened before it runs. Keep them indexed so + // the panel can be rehydrated below — dispatching the bare strings strips + // `providerId` and every panel member loses its provider identity (#8878). + const resolvedByModelStr = new Map(); + for (const target of resolvedFusionTargets) { + if (!resolvedByModelStr.has(target.modelStr)) resolvedByModelStr.set(target.modelStr, target); + } + const { panel: fusionPanel, comboRefUnits } = extractFusionPanelSpec( + resolvedFusionTargets.map((target) => target.modelStr), combo.name, null ); + // A panel entry naming a combo ref stays a string (it is a combo name, not a + // model); everything else regains its resolved target. + const fusionModels = fusionPanel.map((entry) => + comboRefUnits.has(entry) ? entry : (resolvedByModelStr.get(entry) ?? entry) + ); // Untyped like the existing `nestingContext` further down — `nesting` is // already `ComboNestingContext | null` per HandleComboChatOptions, no new // import needed. diff --git a/scripts/build/pack-artifact-policy.ts b/scripts/build/pack-artifact-policy.ts index 54f47487a7..49fcd94e95 100644 --- a/scripts/build/pack-artifact-policy.ts +++ b/scripts/build/pack-artifact-policy.ts @@ -126,6 +126,9 @@ export const PACK_ARTIFACT_ROOT_ALLOWED_EXACT_PATHS: string[] = [ // #7802: imported by scripts/build/postinstall.mjs to repair tls-client-node's // native binary (chatgpt-web/claude-web/grok-web/lmarena/perplexity-web transport). "scripts/build/fixTlsClientNodeBinary.mjs", + // #8859: imported by scripts/build/postinstall.mjs to repair playwright-core's + // browser resolution on Termux/Android (no glibc, no bundled browsers). + "scripts/build/fixPlaywrightAndroid.mjs", // #5227: imported at runtime by bin/cli/commands/serve.mjs (heap auto-calibration). "scripts/build/runtime-env.mjs", "scripts/build/sync-env.mjs", diff --git a/skills/omni-version-manager/SKILL.md b/skills/omni-version-manager/SKILL.md index 85f21839c4..711a8f3e8e 100644 --- a/skills/omni-version-manager/SKILL.md +++ b/skills/omni-version-manager/SKILL.md @@ -116,6 +116,19 @@ curl -X POST https://localhost:20128/api/services/9router/auto-start \ -d '{}' ``` +### POST /api/services/9router/auto-restart-adopted + +Toggle 9Router auto-restart-when-adopted + +When enabled, an externally-adopted (not OmniRoute-spawned) 9Router process is restarted under OmniRoute's own supervisor on the next health-check cycle instead of being left as adopted-only. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/9router/auto-restart-adopted \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + ### POST /api/services/cliproxy/install Install CLIProxyAPI from npm @@ -205,6 +218,19 @@ curl -X POST https://localhost:20128/api/services/cliproxy/auto-start \ -d '{}' ``` +### POST /api/services/cliproxy/auto-restart-adopted + +Toggle CLIProxyAPI auto-restart-when-adopted + +When enabled, an externally-adopted (not OmniRoute-spawned) CLIProxyAPI process is restarted under OmniRoute's own supervisor on the next health-check cycle instead of being left as adopted-only. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/cliproxy/auto-restart-adopted \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + ### POST /api/services/mux/install Install Mux from npm @@ -294,6 +320,19 @@ curl -X POST https://localhost:20128/api/services/mux/auto-start \ -d '{}' ``` +### POST /api/services/mux/auto-restart-adopted + +Toggle Mux auto-restart-when-adopted + +When enabled, an externally-adopted (not OmniRoute-spawned) Mux process is restarted under OmniRoute's own supervisor on the next health-check cycle instead of being left as adopted-only. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/mux/auto-restart-adopted \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + ### POST /api/services/bifrost/install Install Bifrost @@ -383,6 +422,193 @@ curl -X POST https://localhost:20128/api/services/bifrost/auto-start \ -d '{}' ``` +### POST /api/services/bifrost/auto-restart-adopted + +Toggle Bifrost auto-restart-when-adopted + +When enabled, an externally-adopted (not OmniRoute-spawned) Bifrost process is restarted under OmniRoute's own supervisor on the next health-check cycle instead of being left as adopted-only. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/bifrost/auto-restart-adopted \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/dario/install + +Install Dario from npm + +Installs the `@askalf/dario` npm package (Claude-account-pool proxy) under DATA_DIR/services/dario/. Uses execFile (no shell interpolation — hard rule #13). **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/dario/install \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/dario/start + +Start Dario + +Spawns the Dario process. Idempotent if already running. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/dario/start \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/dario/stop + +Stop Dario + +Gracefully stops Dario. Idempotent — returns a stopped status even if no supervisor is currently tracking the process. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/dario/stop \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/dario/restart + +Restart Dario + +Equivalent to stop() then start() under the operation lock. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/dario/restart \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/dario/update + +Update Dario to a newer npm version + +Stops the service (if running), installs the newer npm version, then restarts it if it was running before the update. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/dario/update \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### GET /api/services/dario/status + +Get Dario status + +Returns combined live supervisor state and DB metadata, including the auto-start / auto-restart-adopted flags and whether an update is available. **LOCAL_ONLY** — loopback only. + +```bash +curl https://localhost:20128/api/services/dario/status \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### POST /api/services/dario/auto-start + +Toggle Dario auto-start + +When enabled, Dario starts automatically on the next OmniRoute boot. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/dario/auto-start \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/dario/auto-restart-adopted + +Toggle Dario auto-restart-when-adopted + +When enabled, an externally-adopted (not OmniRoute-spawned) Dario process is restarted under OmniRoute's own supervisor on the next health-check cycle instead of being left as adopted-only. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/dario/auto-restart-adopted \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/dario/admin/login-start + +Start a Dario account-pool login (device-code style) + +Forwards to the running Dario instance's `POST /admin/login/start` using the stored admin token. The operator opens the returned `authorize_url`, approves in their own Claude account, then posts the displayed code to `/admin/login-complete`. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/dario/admin/login-start \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/dario/admin/login-complete + +Complete a Dario account-pool login + +Forwards to the running Dario instance's `POST /admin/login/complete`. On success the account becomes routable immediately (Dario hot-reloads its pool). **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/dario/admin/login-complete \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### GET /api/services/dario/admin/accounts + +List Dario account-pool accounts + +Forwards to the running Dario instance's `GET /admin/accounts`. **LOCAL_ONLY** — loopback only. + +```bash +curl https://localhost:20128/api/services/dario/admin/accounts \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### DELETE /api/services/dario/admin/accounts + +Remove a Dario account-pool account + +Forwards to the running Dario instance's `DELETE /admin/accounts/`. The alias is taken from a `?alias=` query param or a `{ alias }` JSON body. **LOCAL_ONLY** — loopback only. + +```bash +curl -X DELETE https://localhost:20128/api/services/dario/admin/accounts \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### GET /api/services/dario/admin/import-from-omniroute + +List OmniRoute claude connections eligible for Dario import + +Returns eligible OmniRoute `claude` OAuth provider connections (metadata only — id/name/email/org tier, never tokens) so the UI can offer a picker when more than one exists. **LOCAL_ONLY** — loopback only. + +```bash +curl https://localhost:20128/api/services/dario/admin/import-from-omniroute \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### POST /api/services/dario/admin/import-from-omniroute + +Import an OmniRoute claude connection's OAuth tokens into Dario + +Writes the source connection's access/refresh token pair directly into Dario's own account-file store (`~/.dario/accounts/.json`), reusing the shared Claude Code OAuth client_id, then restarts the Dario supervisor so it picks up the new account. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/dario/admin/import-from-omniroute \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + ### GET /api/services/{name}/logs Stream service logs via SSE diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 3f6e9c6b94..d5fc9fea69 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -11537,7 +11537,10 @@ "noSavedProxiesError": "No saved proxies found. Add proxies in Settings → Proxy first.", "updateProviderFailed": "Failed to update provider", "providerEnabled": "{provider} enabled", - "providerDisabled": "{provider} disabled" + "providerDisabled": "{provider} disabled", + "providerAdded": "{provider} added", + "add": "Add", + "manualApiKey": "Use a manual API key" }, "gamification": { "leaderboardScopes": { diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 92dc88b3c9..0aed3399c9 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -5963,7 +5963,8 @@ "kimi-coding": "Conecte o Kimi Coding com o fluxo OAuth existente.", "kiro": "Nível gratuito: 50 créditos/mês (~25K–100K tokens). ⚠️ Os Termos de Serviço (ToS) do Kiro proíbem o uso de proxy/harness de terceiros.", "codex": "Conecte o OpenAI Codex com o fluxo OAuth existente.", - "qwen": "Conecte o Qwen Code com o fluxo OAuth existente." + "qwen": "Conecte o Qwen Code com o fluxo OAuth existente.", + "unorouter": "Crie uma chave de API em https://unorouter.ai e cole aqui como Bearer token." }, "passthroughModelsDescription": "{provider} aceita IDs de modelo nativos do provedor. Importe de /models ou adicione IDs personalizados para roteamento.", "bedrockModelsDescription": "Os modelos Amazon Bedrock têm escopo definido por região da AWS. Importe de /models ou adicione IDs de modelo Bedrock habilitados na região selecionada.", @@ -10858,7 +10859,9 @@ "goNow": "Ir agora", "message": "O MITM Proxy agora vive no AgentBridge.", "title": "Esta página foi movida" - } + }, + "noMappingsDesc": "Nenhum mapeamento de modelo configurado ainda. Adicione mapeamentos para rotear as requisições do agente pelo OmniRoute.", + "addMapping": "Adicionar mapeamento" }, "providerStats": { "unknownError": "Erro desconhecido", @@ -11534,7 +11537,10 @@ "noSavedProxiesError": "Nenhum proxy salvo encontrado. Adicione proxies em Configurações → Proxy primeiro.", "updateProviderFailed": "Falha ao atualizar provedor", "providerEnabled": "{provider} ativado", - "providerDisabled": "{provider} desativado" + "providerDisabled": "{provider} desativado", + "providerAdded": "{provider} adicionado", + "add": "Adicionar", + "manualApiKey": "Usar uma chave de API manual" }, "gamification": { "leaderboardScopes": { diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index 781b669ddf..f6333012d9 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -5955,7 +5955,8 @@ "kimi-coding": "Kết nối Kimi Coding bằng luồng OAuth hiện có.", "kiro": "Gói miễn phí: 50 tín dụng/tháng (khoảng 25–100 nghìn token). ⚠️ Điều khoản Kiro cấm sử dụng proxy/harness của bên thứ ba.", "codex": "Kết nối OpenAI Codex bằng luồng OAuth hiện có.", - "qwen": "Kết nối Qwen Code bằng luồng OAuth hiện có." + "qwen": "Kết nối Qwen Code bằng luồng OAuth hiện có.", + "unorouter": "Tạo khóa API tại https://unorouter.ai, sau đó dán vào đây dưới dạng Bearer token." }, "passthroughModelsDescription": "{provider} chấp nhận ID mô hình gốc của nhà cung cấp. Nhập từ /models hoặc thêm ID tùy chỉnh để định tuyến.", "bedrockModelsDescription": "Các mô hình Amazon Bedrock được giới hạn theo vùng AWS. Nhập từ /models hoặc thêm ID mô hình Bedrock được bật trong vùng đã chọn.", @@ -10850,7 +10851,9 @@ "goNow": "Đi ngay", "message": "MITM Proxy hiện nằm trong AgentBridge.", "title": "Trang này đã được di chuyển" - } + }, + "noMappingsDesc": "Chưa cấu hình ánh xạ mô hình nào. Hãy thêm ánh xạ để định tuyến yêu cầu của agent qua OmniRoute.", + "addMapping": "Thêm ánh xạ" }, "providerStats": { "unknownError": "Lỗi không xác định", @@ -11526,7 +11529,10 @@ "noSavedProxiesError": "Không tìm thấy proxy đã lưu. Hãy thêm proxy trong Cài đặt → Proxy trước.", "updateProviderFailed": "Không thể cập nhật nhà cung cấp", "providerEnabled": "Đã bật {provider}", - "providerDisabled": "Đã tắt {provider}" + "providerDisabled": "Đã tắt {provider}", + "providerAdded": "Đã thêm {provider}", + "add": "Thêm", + "manualApiKey": "Dùng khóa API thủ công" }, "gamification": { "leaderboardScopes": { diff --git a/src/lib/db/migrationRunner.ts b/src/lib/db/migrationRunner.ts index ac6f0ab79d..c5470b6b1e 100644 --- a/src/lib/db/migrationRunner.ts +++ b/src/lib/db/migrationRunner.ts @@ -465,6 +465,13 @@ function isSchemaAlreadyApplied( // exists the rebuild ran — skip re-executing the rename/copy/drop, which // would fail on the missing proxy_assignments_pre117 table. return hasColumn(db, "proxy_assignments", "position"); + // Retroactive guard for the 135/136 renumber (#8523 landed onto slots already taken + // by #8908/#9515): a DB that ran these under the old numbers already has the column, + // and a bare ALTER TABLE ADD COLUMN would throw on the re-run under the new number. + case "137": + return hasColumn(db, "version_manager", "auto_restart_adopted"); + case "138": + return hasColumn(db, "upstream_proxy_config", "fallback_backend"); default: return false; } diff --git a/src/lib/db/migrations/135_auto_restart_adopted.sql b/src/lib/db/migrations/137_auto_restart_adopted.sql similarity index 93% rename from src/lib/db/migrations/135_auto_restart_adopted.sql rename to src/lib/db/migrations/137_auto_restart_adopted.sql index 11f805e7e3..f4177ed350 100644 --- a/src/lib/db/migrations/135_auto_restart_adopted.sql +++ b/src/lib/db/migrations/137_auto_restart_adopted.sql @@ -1,4 +1,4 @@ --- Migration 135: Auto-restart-adopted toggle for embedded services +-- Migration 137: Auto-restart-adopted toggle for embedded services -- -- When a supervisor's probeBeforeSpawn finds a healthy instance already -- listening on its port, it adopts that process instead of spawning a new diff --git a/src/lib/db/migrations/136_dario_fallback_backend.sql b/src/lib/db/migrations/138_dario_fallback_backend.sql similarity index 91% rename from src/lib/db/migrations/136_dario_fallback_backend.sql rename to src/lib/db/migrations/138_dario_fallback_backend.sql index 28315188d3..8dc6f28f73 100644 --- a/src/lib/db/migrations/136_dario_fallback_backend.sql +++ b/src/lib/db/migrations/138_dario_fallback_backend.sql @@ -1,4 +1,4 @@ --- Migration 136: Dario failover backend selection for upstream_proxy_config +-- Migration 138: Dario failover backend selection for upstream_proxy_config -- -- Adds `fallback_backend` so a provider whose mode is 'fallback' can choose -- WHICH embedded proxy handles the retry leg — CLIProxyAPI (the historical, diff --git a/src/lib/services/installers/dario.ts b/src/lib/services/installers/dario.ts index c268fc0383..65cd74d622 100644 --- a/src/lib/services/installers/dario.ts +++ b/src/lib/services/installers/dario.ts @@ -168,19 +168,6 @@ export async function update(): Promise { return install("latest"); } -export async function uninstall(): Promise { - const nmDir = path.join(getDarioInstallDir(), "node_modules"); - if (fs.existsSync(nmDir)) { - fs.rmSync(nmDir, { recursive: true, force: true }); - } - await upsertVersionManagerTool({ - tool: "dario", - status: "not_installed", - installedVersion: null, - binaryPath: null, - }); -} - /** * Build spawn args for ServiceSupervisor.start(). * diff --git a/tests/unit/feature-flags-settings.test.ts b/tests/unit/feature-flags-settings.test.ts index 67fb63951a..c8a8f6d3d4 100644 --- a/tests/unit/feature-flags-settings.test.ts +++ b/tests/unit/feature-flags-settings.test.ts @@ -30,13 +30,13 @@ const { isControlPlaneProxyDirectFallbackEnabled, } = await import("../../src/shared/utils/featureFlags.ts"); -const EXPECTED_FEATURE_FLAG_COUNT = 44; +const EXPECTED_FEATURE_FLAG_COUNT = 45; // ────────────────────────────────────────────────────── // Test group 1 — Flag definitions registry // ────────────────────────────────────────────────────── describe("featureFlagDefinitions", () => { - it("has exactly 44 flag definitions", () => { + it("has exactly 45 flag definitions", () => { assert.strictEqual(FEATURE_FLAG_DEFINITIONS.length, EXPECTED_FEATURE_FLAG_COUNT); }); @@ -332,7 +332,7 @@ describe("resolveFeatureFlag", () => { }); describe("resolveAllFeatureFlags", () => { - it("returns all 44 flags", () => { + it("returns all 45 flags", () => { const all = resolveAllFeatureFlags(); assert.strictEqual(all.length, EXPECTED_FEATURE_FLAG_COUNT); }); diff --git a/tests/unit/providers-constants-split.test.ts b/tests/unit/providers-constants-split.test.ts index e885fae237..32332fc7da 100644 --- a/tests/unit/providers-constants-split.test.ts +++ b/tests/unit/providers-constants-split.test.ts @@ -1,7 +1,7 @@ // Characterization of the providers.ts catalog split (god-file decomposition): the host became a // barrel that re-exports 10 data catalogs now living under constants/providers/*, and APIKEY is // merged from 6 semantic family files (apikey/.ts). Locks: the public surface (every catalog -// + helpers still exported), the spread-merge integrity (196 APIKEY entries, no loss/dup), and that +// + helpers still exported), the spread-merge integrity (197 APIKEY entries, no loss/dup), and that // load-time Zod validation still runs. Pure-data move → behavior must be identical. // Count was 171 before obsolete provider removals (PR #6675: glhf/kluster/cablyai/inclusionai etc., // 171->167) plus #6126 (ClinePass dual-auth): the API-key-only APIKEY_PROVIDERS_GATEWAYS entry was @@ -17,7 +17,7 @@ // sarvam+plamo in regional) to 193, then #8170 (inception/typhoon — inception in frontier-labs, // typhoon in regional) to 195, then Firecrawl dual search+fetch under SEARCH_PROVIDERS.firecrawl // (removed specialty-media duplicate) to 194, #8861 (Xiaomi MiMo Token Plan, regional) to 195, and -// the Cheaper Inference gateway (OSS-sponsor reseller, gateways family) to 196. +// the Cheaper Inference gateway (OSS-sponsor reseller, gateways family) to 197 (UnoRouter, #9009). import { test } from "node:test"; import assert from "node:assert/strict"; @@ -46,12 +46,12 @@ test("barrel still exports every catalog + key helpers", () => { } }); -test("APIKEY_PROVIDERS merges the 6 family files into 196 entries (no loss / no dup)", async () => { +test("APIKEY_PROVIDERS merges the 6 family files into 197 entries (no loss / no dup)", async () => { const keys = Object.keys((P as Record).APIKEY_PROVIDERS); - assert.equal(keys.length, 196); - assert.equal(new Set(keys).size, 196, "duplicate keys after spread-merge"); + assert.equal(keys.length, 197); + assert.equal(new Set(keys).size, 197, "duplicate keys after spread-merge"); // the merged object's entry-count equals the sum of the 6 semantic family files; families are a - // strict partition (every provider in exactly one), so the sum must be exactly 196. + // strict partition (every provider in exactly one), so the sum must be exactly 197. const families: [string, string][] = [ ["gateways", "APIKEY_PROVIDERS_GATEWAYS"], ["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"], @@ -71,7 +71,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 196 entries (no loss / no seen.add(k); } } - assert.equal(famTotal, 196, "families must partition all 196 providers"); + assert.equal(famTotal, 197, "families must partition all 197 providers"); }); test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => { diff --git a/tests/unit/runtime-env.test.ts b/tests/unit/runtime-env.test.ts index 6c98fdfd2c..7a0b1f28b6 100644 --- a/tests/unit/runtime-env.test.ts +++ b/tests/unit/runtime-env.test.ts @@ -40,6 +40,9 @@ test("runtime env helpers normalize runtime ports and conflicting color flags", PORT: "22128", DASHBOARD_PORT: "22128", API_PORT: "21128", + // #8999 (docker entrypoint moved to /app) made withRuntimePortEnv also pin + // the bind address, defaulting to 0.0.0.0 when OMNIROUTE_HOSTNAME is unset. + HOSTNAME: "0.0.0.0", } ); diff --git a/tests/unit/sidebar-costs-section.test.ts b/tests/unit/sidebar-costs-section.test.ts index 8e1dc01532..ffdd5be904 100644 --- a/tests/unit/sidebar-costs-section.test.ts +++ b/tests/unit/sidebar-costs-section.test.ts @@ -12,12 +12,12 @@ test("costs section exists in SIDEBAR_SECTIONS", () => { assert.ok(section, "costs section must exist"); }); -test("costs section has exactly 5 items in the correct order", () => { +test("costs section has exactly 6 items in the correct order", () => { const section = findSection("costs"); assert.ok(section, "costs section must exist"); const items = sidebarVisibility.getSectionItems(section); - assert.equal(items.length, 5, "costs section must have 5 items"); + assert.equal(items.length, 6, "costs section must have 6 items"); const itemIds = items.map((i) => i.id); assert.deepEqual(itemIds, [ @@ -26,6 +26,7 @@ test("costs section has exactly 5 items in the correct order", () => { "costs-budget", "costs-free-tiers", "free-provider-rankings", + "radar", ]); }); @@ -42,6 +43,7 @@ test("costs section items have correct hrefs", () => { { id: "costs-budget", href: "/dashboard/costs/budget" }, { id: "costs-free-tiers", href: "/dashboard/free-tiers" }, { id: "free-provider-rankings", href: "/dashboard/free-provider-rankings" }, + { id: "radar", href: "/dashboard/radar" }, ]); }); diff --git a/tests/unit/tls-client-node-docker-binary-7802.test.ts b/tests/unit/tls-client-node-docker-binary-7802.test.ts index 45fb106f79..ff4aab0a60 100644 --- a/tests/unit/tls-client-node-docker-binary-7802.test.ts +++ b/tests/unit/tls-client-node-docker-binary-7802.test.ts @@ -13,7 +13,10 @@ test("Dockerfile's --ignore-scripts npm ci is compensated for tls-client-node's assert.match( dockerfile, - /npm ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts/, + // Flag-order tolerant on purpose: the assertion is about the --ignore-scripts + // PRECONDITION, not the exact flag list. #9185 inserted --include=optional + // (LLMLingua optional deps) and broke the literal pin without touching intent. + /npm ci(?: --[\w-]+(?:=[\w-]+)?)* --ignore-scripts/, "expected the builder stage to install with --ignore-scripts (precondition of #7802)" );