From 0adae00c7b94c7fd132d4c9fadffaa4ae629bf6d Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Tue, 30 Jun 2026 06:54:29 -0300 Subject: [PATCH] Release v3.8.42 (#5459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release v3.8.42 — full CHANGELOG in CHANGELOG.md. CI: 103 checks green incl. CodeQL (all languages), Semgrep, all 8 unit shards, coverage, Node 24 compat, and integration tests. Full unit suite validated locally: 19437 pass / 0 fail. The 3 red checks are advisory and do not gate main (no required status checks): SonarCloud/SonarQube new-code coverage gate, and PR Test Policy (test-masking detector flagging the legitimate dead-Phind provider removal in #5530 — reviewed, correct). Includes cycle-close reconciliation + repair of inherited base-red tests from #5480/#5527/#5427/#5521 that the PR->release fast-path did not exercise. --- .env.example | 23 +- .github/workflows/npm-publish.yml | 43 ++ .npmrc | 10 + @omniroute/opencode-plugin/package.json | 2 +- AGENTS.md | 4 +- CHANGELOG.md | 92 ++++ CLAUDE.md | 3 +- Dockerfile | 2 +- README.md | 18 +- config/quality/file-size-baseline.json | 10 +- config/quality/quality-baseline.json | 6 +- docs/getting-started/FREE-TIERS-GUIDE.md | 16 +- docs/getting-started/PROVIDERS-GUIDE.md | 4 +- docs/i18n/ar/CHANGELOG.md | 92 ++++ docs/i18n/az/CHANGELOG.md | 92 ++++ docs/i18n/bg/CHANGELOG.md | 92 ++++ docs/i18n/bn/CHANGELOG.md | 92 ++++ docs/i18n/cs/CHANGELOG.md | 92 ++++ docs/i18n/da/CHANGELOG.md | 92 ++++ docs/i18n/de/CHANGELOG.md | 92 ++++ docs/i18n/es/CHANGELOG.md | 92 ++++ docs/i18n/fa/CHANGELOG.md | 92 ++++ docs/i18n/fi/CHANGELOG.md | 92 ++++ docs/i18n/fr/CHANGELOG.md | 92 ++++ docs/i18n/gu/CHANGELOG.md | 92 ++++ docs/i18n/he/CHANGELOG.md | 92 ++++ docs/i18n/hi/CHANGELOG.md | 92 ++++ docs/i18n/hu/CHANGELOG.md | 92 ++++ docs/i18n/id/CHANGELOG.md | 92 ++++ docs/i18n/in/CHANGELOG.md | 92 ++++ docs/i18n/it/CHANGELOG.md | 92 ++++ docs/i18n/ja/CHANGELOG.md | 92 ++++ docs/i18n/ko/CHANGELOG.md | 92 ++++ docs/i18n/mr/CHANGELOG.md | 92 ++++ docs/i18n/ms/CHANGELOG.md | 92 ++++ docs/i18n/nl/CHANGELOG.md | 92 ++++ docs/i18n/no/CHANGELOG.md | 92 ++++ docs/i18n/phi/CHANGELOG.md | 92 ++++ docs/i18n/pl/CHANGELOG.md | 92 ++++ docs/i18n/pt-BR/CHANGELOG.md | 92 ++++ docs/i18n/pt/CHANGELOG.md | 92 ++++ docs/i18n/ro/CHANGELOG.md | 92 ++++ docs/i18n/ru/CHANGELOG.md | 92 ++++ docs/i18n/sk/CHANGELOG.md | 92 ++++ docs/i18n/sv/CHANGELOG.md | 92 ++++ docs/i18n/sw/CHANGELOG.md | 92 ++++ docs/i18n/ta/CHANGELOG.md | 92 ++++ docs/i18n/te/CHANGELOG.md | 92 ++++ docs/i18n/th/CHANGELOG.md | 92 ++++ docs/i18n/tr/CHANGELOG.md | 92 ++++ docs/i18n/uk-UA/CHANGELOG.md | 92 ++++ docs/i18n/ur/CHANGELOG.md | 92 ++++ docs/i18n/vi/CHANGELOG.md | 92 ++++ docs/i18n/zh-CN/CHANGELOG.md | 92 ++++ docs/i18n/zh-TW/CHANGELOG.md | 92 ++++ docs/openapi.yaml | 2 +- docs/reference/ENVIRONMENT.md | 6 +- docs/reference/FREE_TIERS.md | 11 +- docs/reference/PROVIDER_REFERENCE.md | 19 +- electron/package-lock.json | 207 +------ electron/package.json | 4 +- open-sse/config/freeModelCatalog.data.ts | 2 +- open-sse/config/freeTierCatalog.ts | 1 - open-sse/config/providerErrorRules.ts | 13 +- open-sse/config/providers/index.ts | 2 - .../config/providers/registry/bytez/index.ts | 6 +- .../providers/registry/chatgpt-web/index.ts | 19 +- .../providers/registry/friendliai/index.ts | 5 +- .../providers/registry/longcat/index.ts | 11 +- .../config/providers/registry/novita/index.ts | 8 +- .../config/providers/registry/phind/index.ts | 16 - open-sse/executors/base.ts | 7 +- open-sse/executors/chatgpt-web.ts | 515 +++++++++++++++--- open-sse/executors/index.ts | 3 - open-sse/executors/mimocode.ts | 123 ++++- open-sse/executors/muse-spark-web.ts | 2 +- open-sse/executors/phind.ts | 250 --------- open-sse/handlers/chatCore.ts | 5 +- .../chatCore/nonStreamingJsonResponse.ts | 16 + open-sse/package.json | 2 +- .../services/__tests__/tierResolver.test.ts | 2 +- open-sse/services/chatgptTlsClient.ts | 50 +- open-sse/services/combo/quotaStrategies.ts | 465 +++++++--------- .../engines/rtk/commandDetector.ts | 15 + .../engines/rtk/filters/dotnet.json | 47 ++ .../engines/rtk/filters/gradle.json | 47 ++ .../services/compression/languageDetector.ts | 12 +- .../services/compression/pipelineGuards.ts | 45 ++ .../services/compression/rules/de/dedup.json | 38 ++ .../services/compression/rules/de/ultra.json | 107 ++++ .../services/compression/rules/fr/dedup.json | 38 ++ .../services/compression/rules/fr/ultra.json | 107 ++++ .../services/compression/rules/ja/dedup.json | 38 ++ .../services/compression/rules/ja/ultra.json | 86 +++ .../services/compression/rules/zh/dedup.json | 38 ++ .../services/compression/rules/zh/filler.json | 54 ++ .../services/compression/rules/zh/ultra.json | 55 ++ .../services/compression/strategySelector.ts | 25 + open-sse/services/providerCostData.ts | 8 +- open-sse/services/rateLimitManager.ts | 24 +- open-sse/utils/sha3-512.ts | 164 ++++++ open-sse/utils/streamHandler.ts | 18 +- package-lock.json | 56 +- package.json | 4 +- scripts/build/pack-artifact-policy.ts | 8 + src/app/(dashboard)/dashboard/combos/page.tsx | 3 +- .../[id]/ProviderDetailPageClient.tsx | 14 +- .../useModelVisibilityHandlers.test.tsx | 36 +- .../components/PassthroughModelsSection.tsx | 3 +- .../[id]/components/modals/AddApiKeyModal.tsx | 2 +- .../[id]/hooks/modelImportWarning.ts | 17 + .../[id]/hooks/useModelImportHandlers.ts | 14 +- .../[id]/hooks/useModelVisibilityHandlers.ts | 5 +- .../onboarding/providerOnboardingApi.ts | 2 +- .../dashboard/providers/new/page.tsx | 10 +- .../(dashboard)/dashboard/providers/page.tsx | 9 +- .../providers/providerPageStorage.ts | 11 + .../components/CompressionSettingsTab.tsx | 45 +- src/app/api/auth/csrf/route.ts | 14 + src/app/api/db-backups/import/route.ts | 14 +- src/app/api/local/redis/redisRuntime.ts | 46 ++ src/app/api/local/redis/start/route.ts | 46 +- src/app/api/local/redis/stop/route.ts | 48 +- .../health-autopilot/actions/route.ts | 6 + src/app/api/services/9router/install/route.ts | 37 +- src/app/api/services/_shared/installRoute.ts | 79 +++ .../api/services/cliproxy/install/route.ts | 34 +- src/app/api/settings/proxies/route.ts | 138 +---- .../api/v1/_shared/mediaGenerationRoute.ts | 133 +++++ src/app/api/v1/batches/[id]/cancel/route.ts | 32 +- src/app/api/v1/batches/[id]/route.ts | 32 +- src/app/api/v1/batches/formatBatchResponse.ts | 30 + src/app/api/v1/batches/route.ts | 32 +- src/app/api/v1/management/proxies/route.ts | 141 +---- src/app/api/v1/music/generations/route.ts | 89 +-- .../relay/chat/completions/bifrost/route.ts | 49 +- .../relay/chat/completions/bifrostCooldown.ts | 54 ++ .../api/v1/relay/chat/completions/route.ts | 99 ++-- .../relay/chat/completions/routingBackend.ts | 15 +- .../relay/chat/completions/streamFinalizer.ts | 37 ++ src/app/api/v1/videos/generations/route.ts | 88 +-- .../v1/vscode/[token]/familyFirstModelIds.ts | 90 +-- .../v1/vscode/[token]/modelPresentation.ts | 182 +------ .../v1/vscode/[token]/reasoningMetadata.ts | 188 +------ .../v1/vscode/[token]/serviceTierVariants.ts | 202 +------ .../api/v1/vscode/[token]/tokenizedRequest.ts | 57 +- .../vscode/raw/[token]/familyFirstModelIds.ts | 90 +-- .../vscode/raw/[token]/modelPresentation.ts | 182 +------ .../vscode/raw/[token]/reasoningMetadata.ts | 188 +------ .../vscode/raw/[token]/serviceTierVariants.ts | 202 +------ .../v1/vscode/raw/[token]/tokenizedRequest.ts | 57 +- src/app/api/version-manager/request.ts | 51 ++ src/app/api/version-manager/restart/route.ts | 27 +- src/app/api/version-manager/start/route.ts | 29 +- src/app/api/version-manager/stop/route.ts | 27 +- src/i18n/messages/en.json | 15 +- src/i18n/messages/it.json | 2 - src/i18n/messages/ja.json | 2 - src/i18n/messages/ko.json | 2 - src/i18n/messages/mr.json | 2 - src/i18n/messages/ms.json | 2 - src/i18n/messages/nl.json | 2 - src/i18n/messages/no.json | 2 - src/i18n/messages/phi.json | 2 - src/i18n/messages/pl.json | 2 - src/i18n/messages/pt-BR.json | 2 - src/i18n/messages/pt.json | 2 - src/i18n/messages/ro.json | 2 - src/i18n/messages/ru.json | 2 - src/i18n/messages/sk.json | 2 - src/i18n/messages/sv.json | 2 - src/i18n/messages/zh-CN.json | 2 - src/lib/a2a/routingLogger.ts | 49 -- src/lib/agentSkills/schemas.ts | 5 - src/lib/api/proxyRegistryRouteHandlers.ts | 142 +++++ src/lib/arenaEloSync.ts | 1 - src/lib/audit/highLevelActions.ts | 2 - src/lib/batches/types.ts | 2 - src/lib/cloudAgent/types.ts | 21 - src/lib/combos/builderOptions.ts | 268 ++++----- src/lib/copilot/codegraphKnowledge.ts | 45 -- src/lib/db/adapters/driverFactory.ts | 144 +---- src/lib/db/adapters/nodeSqliteAdapter.ts | 162 +----- src/lib/db/adapters/nodeSqliteShared.ts | 148 +++++ src/lib/db/webhookDeliveries.ts | 9 - src/lib/gamification/streaks.ts | 30 - src/lib/headroom/process.ts | 20 +- src/lib/memory/__tests__/schemas.test.ts | 8 +- src/lib/memory/schemas.ts | 7 - src/lib/notion/api.ts | 2 - src/lib/oauth/utils/claudeAuthZipExtract.ts | 94 +--- src/lib/oauth/utils/codexAuthImport.ts | 12 - src/lib/oauth/utils/codexAuthZipExtract.ts | 94 +--- src/lib/promptCache/index.ts | 2 +- src/lib/promptCache/prefixAnalyzer.ts | 4 - src/lib/providers/modelListingCapability.ts | 26 + src/lib/providers/validation.ts | 59 +- src/lib/providers/validation/cozeError.ts | 56 ++ src/lib/providers/validation/openaiFormat.ts | 15 + src/lib/services/installers/ninerouter.ts | 14 +- src/lib/services/installers/utils.ts | 71 ++- src/lib/skills/schemas.ts | 4 - src/lib/skills/skillssh.ts | 1 - src/lib/vscode/familyFirstModelIds.ts | 92 ++++ src/lib/vscode/modelPresentation.ts | 132 +++++ src/lib/vscode/reasoningMetadata.ts | 183 +++++++ src/lib/vscode/serviceTierVariants.ts | 202 +++++++ src/lib/vscode/tokenizedRequest.ts | 56 ++ src/server/authz/csrf.ts | 101 ++++ src/server/authz/pipeline.ts | 3 +- src/server/authz/routeGuard.ts | 33 +- .../components/analytics/rechartsDonuts.tsx | 260 +++------ src/shared/components/lobeProviderIcons.ts | 3 - src/shared/constants/dashboardCsrf.ts | 1 + .../providers/apikey/frontier-labs.ts | 13 - .../providers/apikey/inference-hosts.ts | 13 - .../constants/providers/apikey/regional.ts | 2 +- src/shared/constants/providers/web-cookie.ts | 20 +- src/shared/constants/spawnCapablePrefixes.ts | 35 ++ src/shared/constants/statusColors.ts | 2 - src/shared/providers/webSessionCredentials.ts | 7 - src/shared/schemas/memory.ts | 11 +- src/shared/schemas/playground.ts | 1 - src/shared/schemas/qdrant.ts | 5 - src/shared/schemas/quota.ts | 8 - src/shared/utils/dashboardCsrf.ts | 56 ++ src/shared/utils/logger.ts | 2 - src/shared/utils/machineId.ts | 77 +-- src/shared/validation/schemas/apiV1.ts | 38 +- src/shared/validation/schemas/auth.ts | 11 +- src/shared/validation/schemas/provider.ts | 312 +---------- src/shared/validation/settingsSchemas.ts | 177 +++--- src/sse/services/model.ts | 27 +- src/sse/utils/logger.ts | 14 +- src/types/apiKey.ts | 12 - src/types/combo.ts | 27 - src/types/index.ts | 6 +- src/types/provider.ts | 31 -- src/types/settings.ts | 81 --- src/types/usage.ts | 57 -- tests/integration/integration-wiring.test.ts | 21 +- tests/snapshots/provider/translate-path.json | 35 +- tests/unit/_mocks/settings.ts | 7 +- tests/unit/a2a-routing-logger.test.ts | 32 ++ tests/unit/agentSkills-schemas.test.ts | 24 +- tests/unit/api/v1/bifrost-cooldown.test.ts | 45 ++ .../unit/api/v1/relay-routing-backend.test.ts | 32 ++ .../api/v1/relay-stream-finalizer.test.ts | 46 ++ tests/unit/audit-high-level-actions.test.ts | 7 + tests/unit/authz/csrf.test.ts | 86 +++ tests/unit/authz/pipeline.test.ts | 90 +++ ...spawn-capable-prefixes-client-safe.test.ts | 89 +++ .../unit/bailian-coding-plan-provider.test.ts | 22 + .../base-thinking-budget-config-5312.test.ts | 5 + tests/unit/bytez-validation-5422.test.ts | 45 ++ tests/unit/caveman-packs-de-fr-ja.test.ts | 57 ++ tests/unit/caveman-packs-zh-wenyan.test.ts | 69 +++ ...hatcore-nonstreaming-json-response.test.ts | 22 + .../chatgpt-web-sha3-boringssl-5531.test.ts | 82 +++ tests/unit/chatgpt-web.test.ts | 367 ++++++++++++- .../unit/claude-identity-version-sync.test.ts | 56 ++ .../claude-thinking-tool-choice-guard.test.ts | 5 + tests/unit/cloudAgent-types.test.ts | 22 +- tests/unit/codexAuthZipExtract.test.ts | 31 +- .../combo-builder-model-source-5477.test.ts | 45 ++ ...mpression-pipeline-inflation-guard.test.ts | 125 +++++ tests/unit/compression/bailout.test.ts | 42 +- .../compression/engine-enabled-toggle.test.ts | 13 +- tests/unit/compression/stacked-async.test.ts | 26 +- ...context-editing-executor-injection.test.ts | 54 ++ .../unit/copilot-codegraph-knowledge.test.ts | 34 ++ tests/unit/coze-validation-error-5426.test.ts | 56 ++ .../dashboard/providerPageStorage.test.ts | 229 ++++++++ .../unit/db-adapters/nodeSqliteShared.test.ts | 100 ++++ tests/unit/db-import-ebusy-5406.test.ts | 37 ++ tests/unit/design-grid-background.test.ts | 1 + .../unit/discontinued-providers-2026.test.ts | 17 +- tests/unit/error-message-sanitization.test.ts | 25 + tests/unit/executor-phind.test.ts | 45 -- tests/unit/executor-web-cookie-sweep.test.ts | 1 - tests/unit/formatting-public-surface.test.ts | 3 + tests/unit/free-tier-catalog.test.ts | 14 +- tests/unit/gamification/streaks.test.ts | 10 + tests/unit/headroom-proxy-lifecycle.test.ts | 7 + tests/unit/lib/batches/schemas.test.ts | 5 + tests/unit/limiter-lifecycle.test.ts | 32 ++ tests/unit/local-redis-runtime.test.ts | 55 ++ tests/unit/logger-redaction-wiring.test.ts | 13 +- tests/unit/memory-schemas-roundtrip.test.ts | 15 + tests/unit/mimocode-executor.test.ts | 198 +++++-- .../model-listing-capability-5420.test.ts | 28 + .../unit/muse-spark-cookie-copy-5449.test.ts | 41 ++ tests/unit/notion-api.test.ts | 8 + tests/unit/oauth-kiro.test.ts | 5 + .../unit/onboarding-wizard-route-5427.test.ts | 36 ++ tests/unit/pack-artifact-policy.test.ts | 17 + tests/unit/playground-schemas.test.ts | 11 + .../unit/prompt-cache-prefix-analyzer.test.ts | 30 + ...rovider-add-ux-i18n-import-warning.test.ts | 60 ++ tests/unit/provider-alias-uniqueness.test.ts | 23 +- ...ovider-endpoints-friendliai-novita.test.ts | 38 ++ tests/unit/provider-error-rules.test.ts | 32 +- tests/unit/providers-constants-split.test.ts | 10 +- .../proxy-registry-route-handlers.test.ts | 93 ++++ tests/unit/qdrant-schemas.test.ts | 35 ++ tests/unit/quota-schemas.test.ts | 1 + tests/unit/rtk-gradle-dotnet-filters.test.ts | 37 ++ tests/unit/search-registry.test.ts | 7 + .../services/install-route-helper.test.ts | 121 ++++ .../services/installers/ninerouter.test.ts | 5 +- .../installers/runNpm-shell-5379.test.ts | 74 +++ tests/unit/settings-transform-schema.test.ts | 100 ++++ tests/unit/skills-schemas.test.ts | 46 ++ tests/unit/skills-skillssh.test.ts | 5 + tests/unit/sse-shim-contract.test.ts | 8 + tests/unit/stream-handler.test.ts | 69 +++ .../token-refresh-race-comprehensive.test.ts | 7 + .../unit/types-barrel-model-cooldown.test.ts | 16 + ...ession-settings-tab-consolidation.test.tsx | 114 ++++ tests/unit/version-manager-request.test.ts | 47 ++ tests/unit/vscode-shared-metadata.test.ts | 63 +++ tests/unit/vscode-tokenized-request.test.ts | 46 ++ tests/unit/web-cookie-providers-new.test.ts | 50 -- tests/unit/webhook-deliveries-db.test.ts | 1 + 324 files changed, 11944 insertions(+), 5453 deletions(-) delete mode 100644 open-sse/config/providers/registry/phind/index.ts delete mode 100644 open-sse/executors/phind.ts create mode 100644 open-sse/handlers/chatCore/nonStreamingJsonResponse.ts create mode 100644 open-sse/services/compression/engines/rtk/filters/dotnet.json create mode 100644 open-sse/services/compression/engines/rtk/filters/gradle.json create mode 100644 open-sse/services/compression/pipelineGuards.ts create mode 100644 open-sse/services/compression/rules/de/dedup.json create mode 100644 open-sse/services/compression/rules/de/ultra.json create mode 100644 open-sse/services/compression/rules/fr/dedup.json create mode 100644 open-sse/services/compression/rules/fr/ultra.json create mode 100644 open-sse/services/compression/rules/ja/dedup.json create mode 100644 open-sse/services/compression/rules/ja/ultra.json create mode 100644 open-sse/services/compression/rules/zh/dedup.json create mode 100644 open-sse/services/compression/rules/zh/filler.json create mode 100644 open-sse/services/compression/rules/zh/ultra.json create mode 100644 open-sse/utils/sha3-512.ts create mode 100644 src/app/(dashboard)/dashboard/providers/[id]/hooks/modelImportWarning.ts create mode 100644 src/app/api/auth/csrf/route.ts create mode 100644 src/app/api/local/redis/redisRuntime.ts create mode 100644 src/app/api/services/_shared/installRoute.ts create mode 100644 src/app/api/v1/_shared/mediaGenerationRoute.ts create mode 100644 src/app/api/v1/batches/formatBatchResponse.ts create mode 100644 src/app/api/v1/relay/chat/completions/bifrostCooldown.ts create mode 100644 src/app/api/v1/relay/chat/completions/streamFinalizer.ts create mode 100644 src/app/api/version-manager/request.ts create mode 100644 src/lib/api/proxyRegistryRouteHandlers.ts create mode 100644 src/lib/db/adapters/nodeSqliteShared.ts create mode 100644 src/lib/providers/modelListingCapability.ts create mode 100644 src/lib/providers/validation/cozeError.ts create mode 100644 src/lib/vscode/familyFirstModelIds.ts create mode 100644 src/lib/vscode/modelPresentation.ts create mode 100644 src/lib/vscode/reasoningMetadata.ts create mode 100644 src/lib/vscode/serviceTierVariants.ts create mode 100644 src/lib/vscode/tokenizedRequest.ts create mode 100644 src/server/authz/csrf.ts create mode 100644 src/shared/constants/dashboardCsrf.ts create mode 100644 src/shared/constants/spawnCapablePrefixes.ts create mode 100644 src/shared/utils/dashboardCsrf.ts delete mode 100644 src/types/apiKey.ts delete mode 100644 src/types/combo.ts delete mode 100644 src/types/settings.ts delete mode 100644 src/types/usage.ts create mode 100644 tests/unit/a2a-routing-logger.test.ts create mode 100644 tests/unit/api/v1/bifrost-cooldown.test.ts create mode 100644 tests/unit/api/v1/relay-stream-finalizer.test.ts create mode 100644 tests/unit/authz/csrf.test.ts create mode 100644 tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts create mode 100644 tests/unit/bytez-validation-5422.test.ts create mode 100644 tests/unit/caveman-packs-de-fr-ja.test.ts create mode 100644 tests/unit/caveman-packs-zh-wenyan.test.ts create mode 100644 tests/unit/chatcore-nonstreaming-json-response.test.ts create mode 100644 tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts create mode 100644 tests/unit/claude-identity-version-sync.test.ts create mode 100644 tests/unit/combo-builder-model-source-5477.test.ts create mode 100644 tests/unit/compression-pipeline-inflation-guard.test.ts create mode 100644 tests/unit/copilot-codegraph-knowledge.test.ts create mode 100644 tests/unit/coze-validation-error-5426.test.ts create mode 100644 tests/unit/dashboard/providerPageStorage.test.ts create mode 100644 tests/unit/db-adapters/nodeSqliteShared.test.ts create mode 100644 tests/unit/db-import-ebusy-5406.test.ts delete mode 100644 tests/unit/executor-phind.test.ts create mode 100644 tests/unit/local-redis-runtime.test.ts create mode 100644 tests/unit/model-listing-capability-5420.test.ts create mode 100644 tests/unit/muse-spark-cookie-copy-5449.test.ts create mode 100644 tests/unit/onboarding-wizard-route-5427.test.ts create mode 100644 tests/unit/prompt-cache-prefix-analyzer.test.ts create mode 100644 tests/unit/provider-add-ux-i18n-import-warning.test.ts create mode 100644 tests/unit/provider-endpoints-friendliai-novita.test.ts create mode 100644 tests/unit/proxy-registry-route-handlers.test.ts create mode 100644 tests/unit/qdrant-schemas.test.ts create mode 100644 tests/unit/rtk-gradle-dotnet-filters.test.ts create mode 100644 tests/unit/services/install-route-helper.test.ts create mode 100644 tests/unit/services/installers/runNpm-shell-5379.test.ts create mode 100644 tests/unit/settings-transform-schema.test.ts create mode 100644 tests/unit/skills-schemas.test.ts create mode 100644 tests/unit/types-barrel-model-cooldown.test.ts create mode 100644 tests/unit/ui/compression-settings-tab-consolidation.test.tsx create mode 100644 tests/unit/version-manager-request.test.ts create mode 100644 tests/unit/vscode-shared-metadata.test.ts create mode 100644 tests/unit/vscode-tokenized-request.test.ts diff --git a/.env.example b/.env.example index 1f292cf0de..963c0c58d1 100644 --- a/.env.example +++ b/.env.example @@ -383,6 +383,17 @@ NEXT_PUBLIC_BASE_URL=http://localhost:20128 # is heavy and clients are racing the 30-minute TTL. # OMNIROUTE_CGPT_WEB_IMAGE_CACHE_MAX_MB=256 +# Overall wait budget for a chatgpt-web GPT-5.5 Pro background-poll handoff, +# in milliseconds. Default 1200000 (20 minutes). Pro reasoning runs are slow +# and complete out-of-band, so OmniRoute polls until the answer lands or this +# budget elapses. Raise it if Pro requests time out before finishing. +# OMNIROUTE_CGPT_WEB_PRO_TIMEOUT_MS=1200000 + +# Interval between chatgpt-web GPT-5.5 Pro background-poll attempts, in +# milliseconds. Default 4000 (4 seconds). Lower for snappier completion at the +# cost of more upstream polling; raise to reduce request volume. +# OMNIROUTE_CGPT_WEB_PRO_POLL_INTERVAL_MS=4000 + # Public cloud URL — client-side mirror of CLOUD_URL. NEXT_PUBLIC_CLOUD_URL= @@ -964,6 +975,10 @@ CURSOR_USER_AGENT="Cursor/3.4" # layered on top of it when the native library is wedged. # OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS=60000 # OMNIROUTE_CHATGPT_TLS_GRACE_MS=10000 +# Max wait for the FIRST streamed byte from the ChatGPT TLS sidecar before the +# request is aborted as a dead stream, in milliseconds. Default 30000 (30s). +# Raise it if upstream cold-starts routinely exceed the window. +# OMNIROUTE_CHATGPT_STREAM_FIRST_BYTE_TIMEOUT_MS=30000 # ── Claude TLS sidecar (Chromium-fingerprinted client) ── # Used by: open-sse/services/claudeTlsClient.ts — wire-level timeout for @@ -1784,7 +1799,7 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis # Per-request timeout when proxying to the Bifrost gateway. Default: 30000 (30s). # BIFROST_TIMEOUT_MS= # Alias for BIFROST_API_KEY (used by scripts that read the env via -# OMNIROUTE_*). Falls back to BIFROST_API_KEY when unset. +# OMNIROUTE_*). BIFROST_API_KEY takes precedence when both are set. # OMNIROUTE_BIFROST_KEY= # Relay backend selection for the OpenAI-compatible relay endpoint: # ts | bifrost | auto. "ts" (default when Bifrost is not configured) uses the @@ -1796,6 +1811,10 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis # and X-Routing-Fallback. # OMNIROUTE_RELAY_BACKEND= # RELAY_ROUTING_BACKEND= +# Cooldown (ms) after a Bifrost sidecar hop fails in "auto" mode before the relay +# re-attempts the sidecar; it goes straight to the TS path while the cooldown lasts. +# 0 disables. Default 5000. Only applies when OMNIROUTE_RELAY_BACKEND=auto. +# OMNIROUTE_BIFROST_FAILURE_COOLDOWN_MS= # Opt-in native HTTPS/TLS for `omniroute serve` (equivalent to --tls-cert / # --tls-key). Provide BOTH a PEM certificate and its private key and the # standalone server terminates TLS on the same listener (wss:// works @@ -1850,6 +1869,8 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis # gateway as a separate sidecar (helps in 3+ replica deployments where you want # provider rotation centralised). See docs/architecture/cluster-decisions.md § # "Bifrost (bifrost profile)". +# Set OMNIROUTE_RELAY_BACKEND=auto to use this sidecar when healthy, or +# OMNIROUTE_RELAY_BACKEND=bifrost to require it without TS fallback. # BIFROST_BASE_URL=http://bifrost:8080 # BIFROST_API_KEY= # BIFROST_STREAMING_ENABLED=true diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 354381d4fe..8e74b96d46 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -205,6 +205,8 @@ jobs: uses: actions/checkout@v7 with: persist-credentials: false + fetch-depth: 0 + # Full history needed for auto-bump: git diff against previous release tag - name: Setup Node.js uses: actions/setup-node@v6 @@ -212,6 +214,47 @@ jobs: node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }} registry-url: https://registry.npmjs.org + - name: Auto-bump plugin version if plugin changed since last release + id: bump + working-directory: "@omniroute/opencode-plugin" + env: + CURRENT_TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + + PKG_VERSION=$(node -p "require('./package.json').version") + PKG_NAME=$(node -p "require('./package.json').name") + + # 1) Skip if current version is not yet published (no bump needed) + PUBLISHED="$(npm view "${PKG_NAME}@${PKG_VERSION}" version 2>/dev/null || true)" + if [ "$PUBLISHED" != "$PKG_VERSION" ]; then + echo "✅ ${PKG_NAME}@${PKG_VERSION} is new — no bump needed." + echo "bumped=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # 2) Find the previous release tag (exclude the current one) + PREV_TAG=$(git tag -l 'v*' --sort=-version:refname \ + | grep -v "^${CURRENT_TAG}$" | head -1 || echo "") + if [ -z "$PREV_TAG" ]; then + echo "No previous tag to compare — skipping bump." + echo "bumped=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # 3) Check if plugin dir actually changed since that tag + if git diff --quiet "$PREV_TAG" -- "@omniroute/opencode-plugin/"; then + echo "⏭️ No plugin changes since $PREV_TAG — nothing to publish." + echo "bumped=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # 4) Auto-bump patch version + npm version patch --no-git-tag-version --allow-same-version + NEW_VERSION=$(node -p "require('./package.json').version") + echo "bumped=true" >> "$GITHUB_OUTPUT" + echo "📦 Auto-bumped ${PKG_NAME} from ${PKG_VERSION} to ${NEW_VERSION}" + - name: Install plugin dependencies working-directory: "@omniroute/opencode-plugin" run: npm install --no-audit --no-fund diff --git a/.npmrc b/.npmrc index e746658575..c6dba73266 100644 --- a/.npmrc +++ b/.npmrc @@ -2,3 +2,13 @@ # Keeping peer auto-install disabled prevents npm from pulling @lobehub/ui/mermaid # back into the tree and reopening npm audit findings for unused packages. legacy-peer-deps=true + +# Network resilience: enlarge npm's fetch retry budget so a transient registry +# socket reset (ECONNRESET) mid-download retries instead of failing the job. +# npm defaults to only 2 retries with short timeouts; `npm ci` in +# electron-release.yml hit ECONNRESET during v3.8.41 publish. Applies to every +# CI workflow (electron / docker / unit) and local installs. +fetch-retries=5 +fetch-retry-factor=4 +fetch-retry-mintimeout=20000 +fetch-retry-maxtimeout=120000 diff --git a/@omniroute/opencode-plugin/package.json b/@omniroute/opencode-plugin/package.json index e2f2bbde4c..491de78c16 100644 --- a/@omniroute/opencode-plugin/package.json +++ b/@omniroute/opencode-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@omniroute/opencode-plugin", - "version": "0.1.0", + "version": "0.2.0", "description": "OpenCode plugin for the OmniRoute AI Gateway. Drives dynamic model discovery, /connect auth flow, and multi-instance OmniRoute providers via the official @opencode-ai/plugin contract.", "type": "module", "main": "./dist/index.js", diff --git a/AGENTS.md b/AGENTS.md index ce665709a4..289ed00296 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,12 +3,12 @@ ## Project Unified AI proxy/router — route any LLM through one endpoint. Multi-provider support -with **237 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks, +with **236 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks, Cohere, NVIDIA, Cerebras, Pollinations, Puter, Cloudflare AI, HuggingFace, DeepInfra, SambaNova, Meta Llama API, Moonshot AI, AI21 Labs, Databricks, Snowflake, and many more) with **MCP Server** (94 tools), **A2A v0.3 Protocol**, and **Electron desktop app**. -> **Live counts (v3.8.40)**: providers 237 · MCP tools 94 · MCP scopes 30 · A2A skills 6 · +> **Live counts (v3.8.40)**: providers 236 · MCP tools 94 · MCP scopes 30 · A2A skills 6 · > open-sse services 298 · routing strategies 17 · auto-combo scoring factors 12 · > DB modules 94 · DB migrations 106 · base tables 17 · search providers 11 · > i18n locales 42. **Refresh with `npm run check:docs-all`.** diff --git a/CHANGELOG.md b/CHANGELOG.md index 550e93ad4d..32c5c413f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,98 @@ --- +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/CLAUDE.md b/CLAUDE.md index d21aa5a08b..b5b377e4bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,7 @@ For full test matrix, see `CONTRIBUTING.md` → "Running Tests". For deep archit ## Project at a Glance -**OmniRoute** — unified AI proxy/router. One endpoint, 237 LLM providers, auto-fallback. +**OmniRoute** — unified AI proxy/router. One endpoint, 236 LLM providers, auto-fallback. | Layer | Location | Purpose | | ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -539,6 +539,7 @@ the stale-enforcement added in Fase 6A.3. 18. Every bug fix must be validated before shipping: a failing-then-passing unit/integration test (TDD) OR a documented live test on the production VPS (192.168.0.15). A fix without either is not merged. See Testing → "Bug fix / issue triage protocol" for the full decision tree. 19. Never develop on the shared main checkout. Every development task runs in its own git worktree on its own dedicated branch, and you MUST confirm the base branch with the operator (e.g. via `AskUserQuestion`) before creating the worktree/branch — never assume `main` or the currently checked-out branch. A `git checkout` in the shared checkout silently destroys other sessions' uncommitted work. Tear down only the worktrees/branches you created (by name, never `fix/*`/`feat/*` wildcards), leave other sessions' worktrees untouched, and end on the branch you started on (the active `release/vX.Y.Z`, never `main`). See Git Workflow → "Worktree isolation". 20. PII redaction/sanitization is **opt-in — never on by default**. OmniRoute proxies for self-hosted/local LLMs where the operator owns the data, so mutating request/response payloads by default would silently corrupt legitimate traffic. The two data-mutating PII feature flags **MUST** keep `defaultValue: "false"` in `src/shared/constants/featureFlagDefinitions.ts`: `PII_REDACTION_ENABLED` (request-side) and `PII_RESPONSE_SANITIZATION` (response + streaming). All three application points — `src/lib/guardrails/piiMasker.ts` (request guardrail), `src/lib/piiSanitizer.ts` (response), `src/lib/streamingPiiTransform.ts` (SSE) — are gated on these flags; with both off the `pii-masker` guardrail still runs but never mutates payloads (data passes through untouched). Flipping either default to `"true"` requires explicit operator approval. The regression guard is `tests/unit/pii-opt-in-default.test.ts` (asserts both definition defaults + behavioral pass-through). Opt-in is per-operator via env or the settings/DB override (`src/lib/db/featureFlags.ts`), never a silent default. See `docs/security/GUARDRAILS.md`. +21. **Release-freeze — the release branch is frozen to campaign merges while a `/generate-release` is running.** `/generate-release` opens a marker issue labeled `release-freeze` at the start of reconciliation (Phase 0a) and closes it once the release PR squash-merges to `main`. Before merging **any** PR into the active `release/vX.Y.Z` branch, every campaign workflow (`/review-issues`, `/review-prs`, `/implement-features`, `/green-prs`, `/port-upstream-*`) **MUST** check `gh issue list --repo diegosouzapw/OmniRoute --label release-freeze --state open` — if a freeze is active, **HOLD the merge** (leave the PR ready and open; do NOT merge to the release branch), tell the operator, and resume once the freeze lifts. This is a **coordination signal, not a permission lock**: the release captain and the campaign sessions share the `diegosouzapw` identity, so a GitHub branch-protection lock cannot distinguish them — only this honored marker prevents the mid-release commit races that forced full CHANGELOG re-reconciliation in v3.8.40/v3.8.41 (a parallel campaign advanced `release/vX.Y.Z` by 34 commits mid-run). The release captain's own reconciliation/cycle-open pushes are exempt — they *are* the release. Fixes that must land during a freeze (a homologation finding) follow the post-merge read-only rule: land on `main` first via `fix/release-vX.Y.Z-*`. --- diff --git a/Dockerfile b/Dockerfile index 28568268db..01f54c1bd1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,7 +39,7 @@ COPY package*.json ./ # Workspace package manifests MUST be present before `npm ci` so npm materializes # the workspace and installs its *workspace-only* deps (e.g. safe-regex, # @toon-format/toon — declared in open-sse/package.json, not hoisted to root). -# Without this, `npm ci` skips them and `npm run build` fails with "Module not +# Without this, `npm ci` skips them and the application build fails with "Module not # found" (root cause of the v3.8.39 Docker build break). workspaces = ["open-sse"]. COPY open-sse/package.json ./open-sse/package.json COPY scripts/build/postinstall.mjs ./scripts/build/postinstall.mjs diff --git a/README.md b/README.md index 7d82821ca9..d17a351455 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # 🚀 OmniRoute — The Free AI Gateway -### Never stop coding. Connect every AI tool to **237 providers** — **50+ free** — through one endpoint. +### Never stop coding. Connect every AI tool to **236 providers** — **50+ free** — through one endpoint. **Plug Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini. Auto-fallback.**
@@ -138,11 +138,11 @@ -> One endpoint. **237 providers.** Never stop building — and let OmniRoute pick the cheapest one that works. +> One endpoint. **236 providers.** Never stop building — and let OmniRoute pick the cheapest one that works. - + @@ -301,7 +301,7 @@ Result: 4 layers of fallback = zero downtime - **💸 Cost telemetry everywhere** — `X-OmniRoute-*` cost/usage headers on every endpoint (including media), a non-token cost engine, a cache-HIT `X-OmniRoute-Cost-Saved` header, and per-key USD spend quotas. → [API Reference](docs/reference/API_REFERENCE.md) - **🧠 Memory you control** — opt-in int8 vector quantization (Qdrant + sqlite-vec), memory off by default, and a per-request `x-omniroute-no-memory` header. → [Memory](docs/frameworks/MEMORY.md) - **🛡️ Security** — a prompt-injection guard across every LLM route (backed by a red-team suite), plus a free DuckDuckGo last-resort web search. → [Guardrails](docs/security/GUARDRAILS.md) -- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 237-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech / transcription / music / video), and one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`). → [Providers](docs/reference/PROVIDER_REFERENCE.md) +- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 236-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech / transcription / music / video), and one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`). → [Providers](docs/reference/PROVIDER_REFERENCE.md) - **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel), one-click **Cloudflare Workers** and **Deno Deploy** relay deployers wired into the proxy pool, and an optional Bifrost Go sidecar that offloads the hottest relay path (`BIFROST_BASE_URL`, with automatic fallback to the TypeScript path on timeout) — now with a relay-backend selector (`OMNIROUTE_RELAY_BACKEND=ts|bifrost|auto`) so the `/v1/relay` endpoint stays the stable surface while choosing the fastest backend internally. → [Environment](docs/reference/ENVIRONMENT.md)
@@ -348,7 +348,7 @@ Result: 4 layers of fallback = zero downtime -> The most complete catalog of any open-source router: **237 providers**, **50+ with a free tier**, **11 free forever**. +> The most complete catalog of any open-source router: **236 providers**, **50+ with a free tier**, **11 free forever**.
@@ -359,7 +359,7 @@ Result: 4 layers of fallback = zero downtime
- + @@ -726,7 +726,7 @@ podman compose --profile base up -d | **Qoder** | `if/` | kimi-k2-thinking, qwen3-coder-plus, deepseek-r1 | ♾️ Unlimited | | **Qwen** | `qw/` | qwen3-coder-plus/flash/next | ♾️ Unlimited | | **Pollinations** | `pol/` | GPT-5, Claude, Gemini, DeepSeek, Llama 4 | No key needed | -| **LongCat** | `lc/` | LongCat-Flash-Lite | 50M tokens/day 🔥 | +| **LongCat** | `lc/` | LongCat-2.0 | 10M one-time (KYC) | | **Cloudflare AI** | `cf/` | 50+ models | 10K neurons/day | | **NVIDIA NIM** | `nvidia/` | 129 models | ~40 RPM | | **Cerebras** | `cerebras/` | Qwen3 235B, GPT-OSS 120B | 1M tok/day | @@ -748,7 +748,7 @@ podman compose --profile base up -d 1. kr/claude-sonnet-4.5 (Kiro — ~50 credits/mo per acct) 2. if/kimi-k2-thinking (Qoder — unlimited) 3. pol/gpt-5 (Pollinations — no key) -4. lc/longcat-flash-lite (50M tok/day backup) +4. lc/LongCat-2.0 (10M one-time backup, KYC) Compression: aggressive (~50%) → double your free quota · Cost: $0/mo ``` @@ -804,7 +804,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo **Will I be charged by OmniRoute?** No — it's free, open-source software on your machine. You only pay paid providers directly. OmniRoute has no billing system. **Are FREE providers really unlimited?** Mostly — Qoder, Pollinations, LongCat, and Cloudflare are free with no per-account credit cap. Kiro is free too but capped at ~50 credits/month per account. Stack multiple free providers in a combo and auto-fallback keeps you serving for $0. **Will compression hurt quality?** No — it only compresses the **input**; code, URLs, JSON are always protected. -**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 237 providers. +**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 236 providers. 📖 [User Guide](docs/guides/USER_GUIDE.md) · [API Reference](docs/reference/API_REFERENCE.md) · [Environment Config](docs/reference/ENVIRONMENT.md) diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 5890dfa4ba..e957dfe278 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -31,6 +31,8 @@ "_rebaseline_2026_06_21_4489_sticky_weighted_limit": "PR #4489 (adivekar-utexas) own growth: src/app/(dashboard)/dashboard/combos/page.tsx 4385->4386 (+1 = the new Sticky Weighted Limit input in the weighted advanced section, a single FieldLabelWithHelp + number block gated to strategy===weighted, analogous to the existing stickyRoundRobinLimit field for round-robin) and open-sse/services/combo.ts ->2761 (sticky-weighted feature: weightedStickyTargets state wiring + the isTargetSelectableForWeighted availability pre-filter + exhaustion-aware sticky clearing/migration in handleComboChat + the round-robin sticky pre-filter in handleRoundRobinCombo + the 4 gemini-review fixes — provider guard, startsWith-separator key match, stepGroups dedup, stale-sticky cleanup). Frozen to the merge-tree measurement (release/v3.8.33 base 2649 + the feature) since CI measures the merged tree, not the v3.8.32-based branch tip. Cohesive routing logic at the existing combo dispatch chokepoints; not a movable block. Structural shrink of combo.ts tracked in #3501. Covered by tests/unit/combo-strategy-fallbacks.test.ts (sticky-weighted batching/fallback-migration/stale-clear/nested-availability + RR sticky-clear) and tests/unit/combo-config.test.ts (stickyWeightedLimit schema).", "_rebaseline_2026_06_20_reviewprs_mine_r2_filesize": "Reconciliacao file-size pos-lote /review-prs 'apenas minhas' r2: dois frozen cresceram cumulativamente sem bump (cada PR media OK na sua base, mas o crescimento empilhou acima do frozen no tip de merge; o fast-path do release nao roda check:file-size, so release->main). (1) src/shared/constants/pricing.ts 1620->1623 (+3 = linhas de pricing Claude Code (cc) do #4440, sobre o 1620 que o #4447 ja setara para gpt-4.1-mini/nano + o3/o4-mini). (2) open-sse/executors/base.ts 1399->1407 (+8 = handling granular de reasoning_effort para Claude no Copilot do #4443). Ambos dados/wiring coesos nos chokepoints existentes; nao extraiveis. Cobertos por tests/unit (claude-code pricing / base-executor-sanitize-effort + github-claude-reasoning-effort-granular).", "_rebaseline_2026_06_22_4647_opencode_go_deepseek": "PR #4647 (DevEstacion/opencode-go DeepSeek V4 Pro effort variants) review feedback: open-sse/executors/base.ts 1407->1414 (+7 = supportsMaxEffortForProvider now opt-ins opencode-go+deepseek so the literal 'max' effort survives the post-transformReasoningEffortForProvider pass — without this, max was silently rewritten to xhigh (OmniRoute's internal top tier) and the opencode-go upstream rejected it. The check is scoped to opencode-go deliberately to preserve the OpenRouter-DeepSeek inverse invariant (pi#4055, asserted by base-executor-sanitize-effort test:OpenRouter DeepSeek normalizes max -> xhigh). The +5 explanatory comment is required: a naive maintainer could otherwise broaden the check to all deepseek models and break the OpenRouter contract. Cohesive at the existing supportsMaxEffortForProvider chokepoint, next to the Claude/CC-compatible check; not extractable. Covered by tests/unit/base-executor-sanitize-effort.test.ts (3 new opencode-go deepseek cases).", + "_rebaseline_2026_06_30_v3842_release_basetsl_5480": "v3.8.42 cycle-close file-size reconciliation: open-sse/executors/base.ts 1497->1500 (+3 net = #5480 'gate claude adaptive thinking defaults' — the adaptive-thinking injection is now gated behind the operator's thinking-budget config at the existing transform chokepoint, so default/passthrough no longer force-injects). Cohesive at the existing reasoning/thinking transform site; not extractable. The fast-path release gate (PR->release/**) does not run check:file-size, so this surfaced only on the release PR (PR->main). Covered by tests/unit/base-thinking-budget-config-5312.test.ts + the #5480 gate test.", + "_rebaseline_2026_06_30_v3842_release_chatgptweb_compression": "v3.8.42 cycle-close file-size reconciliation (DRIFT measured OK on each PR's base, stacked above frozen at the merge tip; fast-path PR->release/** does not run check:file-size). (1) open-sse/executors/chatgpt-web.ts 2870->3206 (+336 = #5531 portable SHA3-512 sentinel-PoW wiring with the native-vs-fallback digest path + #5536 GPT-5.5 Pro handoff branch; the pure Keccak-f[1600] fallback itself already lives in the separate leaf open-sse/utils/sha3-512.ts — the executor growth is the cohesive call-site/handoff logic, not extractable without hiding the sentinel chokepoint). (2) tests/unit/chatgpt-web.test.ts 2855->3159 (+304 = #5536 GPT-5.5 Pro handoff coverage; pair-file with its executor). (3) open-sse/services/compression/strategySelector.ts 997->1022 (+25 = #5527 T02 honest default-on pipeline inflation guard wiring at the existing finalizeStackedResult choke). All cohesive at existing chokepoints; covered by tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts, chatgpt-web.test.ts (GPT-5.5 Pro), compression-pipeline-inflation-guard.test.ts.", "_rebaseline_2026_06_20_4023_web_cookie_noauth_validation": "PR #4023 (oyi77) own growth: src/lib/providers/validation.ts 4450->4518 (+68 = a new validateWebCookieProvider that probes the provider's /models endpoint — 401/403 => AUTH_007 SESSION_EXPIRED, any other status => valid session, empty cookie => invalid, provider-not-in-registry => unsupported — plus a local STANDARD_USER_AGENT const for the probe). Cohesive validator at the validateProviderApiKey dispatch; not extractable. Covered by tests/unit/provider-validation-web-cookie-auth007.test.ts. Heavily curated on merge — the PR's branch was badly stale-based (squash-base-stale), so its tree was DESTRUCTIVE: providers/index.ts deleted live providers openadapter/dit/tokenrouter (added by #4313) and the executor/base.ts edits reverted release fixes (#4037 duckduckgo host, theoldllm gpt5 models, base.ts fetch-start-timeout). Only the purely-additive validation feature was kept (validation.ts validateWebCookieProvider + errorCodes AUTH_007 + the test). Dropped: 5 malformed new registry entries (used non-RegistryEntry fields defaultModel/auth + referenced non-existent executors -> tsc TS2353), the destructive providers/index.ts + executor reverts, the unrelated pr-*.sh automation scripts, and evals/types.ts (belongs to the deferred evals modularization #4422). Also removed the PR's fragile 'Phase 2' executor probe (ran a live upstream chat during validation + classified any 'auth'-containing error as SESSION_EXPIRED) and rewrote the test to install its fetch mock before module load (the original mocked too late and silently hit live chatgpt.com).", "_rebaseline_2026_06_20_1308_model_lockout_honors_reset": "port from 9router#1308 own growth: open-sse/services/accountFallback.ts 1731->1752 (+21 = the new exported pure helper selectLockoutCooldownMs + its doc comment — picks the parsed upstream reset as the model-lockout exactCooldownMs when it exceeds the base cooldown, e.g. Antigravity \"Resets in 160h\", else preserves the existing 0/base behavior) and open-sse/executors/antigravity.ts 1680->1686 (this PR +1 = parseRetryFromErrorMessage regex `reset` -> `resets?` so plural \"Resets in 160h27m24s\" matches, plus a comment line; frozen set to the SUM 1686 with the concurrent #1944 which adds +5 at the disjoint passthroughFields region of the same file, so either merge order passes — pair-file rule). The combo lockout call sites in combo.ts now pass selectLockoutCooldownMs(cooldownMs, mlSettings) instead of always base/exponential, so an exhausted model honors the real upstream reset instead of being retried within minutes. Both edits are cohesive at the existing lockout/parse chokepoints; the helper is its own pure function (not extractable further). Covered by tests/unit/combo-model-lockout-honors-reset-1308.test.ts.", "_rebaseline_2026_06_20_1944_antigravity_strip_output_config": "port from 9router#1944: open-sse/executors/antigravity.ts frozen set to the measured cumulative 1687 of two concurrent PRs that touch disjoint regions of this file, so either merge order passes (pair-file rule). #1944 adds +6 at the envelope passthroughFields destructuring (~line 759: drop output_config/output_format — Anthropic/Claude-Code-only fields that Google's Cloud Code envelope rejects with `400 Unknown name \"output_config\"`, which broke every Claude model on Antigravity); #1308 adds +1 at parseRetryFromErrorMessage (~line 889: regex reset->resets?). Base 1680 + 6 + 1 = 1687 (re-measured on the real merge tip — the earlier 1686 estimate was off by one). Both edits are cohesive at their chokepoints; not extractable. Covered by tests/unit/antigravity-strip-output-config-1944.test.ts.", @@ -128,8 +130,8 @@ "_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).", "open-sse/config/providerRegistry.ts": 4731, "open-sse/executors/antigravity.ts": 1806, - "open-sse/executors/base.ts": 1497, - "open-sse/executors/chatgpt-web.ts": 2870, + "open-sse/executors/base.ts": 1500, + "open-sse/executors/chatgpt-web.ts": 3206, "open-sse/executors/claude-web.ts": 1057, "open-sse/executors/codex.ts": 1541, "open-sse/executors/cursor.ts": 1577, @@ -164,7 +166,7 @@ "_rebaseline_2026_06_26_fidelity_gate_extraction": "Milestone-B fidelity-gate wiring residual: bodyToText+gateAdvance extracted to fidelityGateStep.ts (889->854, -35), but the StackOptions.fidelityGate field, the `const fidelityGate` reads at the two stacked-loop dispatch chokepoints, and the import of FidelityGateConfig are irreducible wiring that cannot leave strategySelector without an architectural refactor of the pre-existing stacked pipeline. Net: 889->854 (+6 vs the pre-Milestone-B frozen 848). Covered by tests/unit/compression/*.test.ts (940 pass).", "_rebaseline_2026_06_28_5243_risk_gate_prepass": "PR #5243 (compression risk-gate pre-pass) own growth: open-sse/services/compression/strategySelector.ts 854->899 (+45). The three exported entry points (applyCompression/applyStackedCompression/applyStackedCompressionAsync) become thin wrappers over pure-extracted private bodies (runCompression/runStackedCompression/runStackedCompressionAsync) so the risk-gate mask->run->restore wrapper sits strictly OUTSIDE the per-step loop — a single universal integration point. The wrapper logic itself (resolveRiskGate/withRiskGate) lives in the new riskGate/strategyWrap.ts (960 (+61 = the opt-in result-memoization branches in applyCompression/applyCompressionAsync — principal+determinism gate, makeMemoKey lookup/store with model+supportsVision folded into the key, recompute-with-memo-off). Default off (memoizeCompressionResults), so zero behavior change. The memo helpers live in the leaf resultMemo.ts (2181 (+78 = the compare-and-swap guard on the refresh persist — runWithCasGuard/getActiveCasGuard AsyncLocalStorage pair mirroring runWithOnPersist, casGuardShouldSkipPersist that rereads the row right before persisting and skips the write when a concurrent writer already rotated the refresh_token past the one presented, plus getCasGuardStats counters). Fixes the sibling-rotation-revert → token-family-revocation storm. Gated behind an active guard (opt-in; no guard => byte-identical). Wiring lives at the two persist chokepoints inside getAccessToken; the comparison reuses wasRefreshTokenRotated from refreshSerializer. Not extractable without splitting the refresh hot path.", "open-sse/services/tokenRefresh.ts": 2181, @@ -265,7 +267,7 @@ "tests/unit/cc-compatible-provider.test.ts": 1179, "tests/unit/chatcore-sanitization.test.ts": 829, "tests/unit/chatcore-translation-paths.test.ts": 2810, - "tests/unit/chatgpt-web.test.ts": 2855, + "tests/unit/chatgpt-web.test.ts": 3159, "tests/unit/combo-routing-engine.test.ts": 3213, "tests/unit/combo-strategy-fallbacks.test.ts": 880, "tests/unit/db-core-init.test.ts": 877, diff --git a/config/quality/quality-baseline.json b/config/quality/quality-baseline.json index 06616fef14..9b4c150fa7 100644 --- a/config/quality/quality-baseline.json +++ b/config/quality/quality-baseline.json @@ -2,8 +2,9 @@ "_comment": "Catraca de qualidade. 'down' = nao pode aumentar; 'up' = nao pode cair. Atualize via 'npm run quality:ratchet -- --update' (somente quando melhora). Cada valor e um numero REAL medido, nunca um chute. Cobertura entra na Fase 4 a partir de um run de cobertura mergeada no CI.", "metrics": { "eslintWarnings": { - "value": 4116, + "value": 4121, "direction": "down", + "_rebaseline_2026_06_30_v3842_release": "4116->4121 (+5). v3.8.42 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's 90 commits — chatgpt-web PoW sha3-512 BoringSSL fix #5540, provider baseUrl/i18n umbrella #5511, proxy union proxyUrlMap+acct.proxy #5521, dead-code + duplication waves #5468-#5495, tls-options packaging #5503, release-freeze + .npmrc fetch-retries #5506, dast-smoke spawn-prefix client-safe extraction #5546, plus ~30 SSE/translator/combo/dashboard fixes). Trust-but-verify: measured 4121 via `npm run check:release-green` on the working tree INCLUDING my reconciliation (CHANGELOG/i18n/golden snapshot + file-size baseline) — those touch only config JSON + a provider snapshot (eslint-ignored) and contribute 0 warnings; all +5 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.", "_rebaseline_2026_06_29_v3841_release": "4103->4116 (+13). v3.8.41 cycle drift surfaced by the release-green collect (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's 52 commits — relay backend #5315, gemini catalog #5337, services dashboard #5299, empty-Claude-messages guard #5342, thinking-budget/redacted-replay + marker opt-out #5312/#5352/#5367, opencode proxy-pool + observability #5217/#5370/#5351, cors + HTTPS-serve #5242/#5360/#5361, grok cf_clearance #5350/#5358, oauth/chatgpt-web/routing/cli/dashboard/rerank #5326/#5240/#5239/#5238/#5264/#5332, partially offset by the dead-code sweep #5321-#5371). Trust-but-verify: measured 4116 via `npm run quality:collect` on the working tree INCLUDING my reconciliation (CHANGELOG/i18n/README/env docs + baselines) AND the lint-fix in useServiceLogs.ts — that fix REMOVES a setState-in-effect ERROR (eslintErrors stays 0) and adds an `open` listener with no `any`/unused, contributing 0 warnings; all +13 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.", "_rebaseline_2026_06_29_v3840_release": "4090->4103 (+13). v3.8.40 cycle drift surfaced by the release-green pre-flight + the release PR Quality Ratchet (the ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's ~57 commits — compression roadmap relevance/hard-budget/memoization/transparency/saliency/splitter/tool_search/RTK/QuantumLock #5289/#5288/#5286/#5284/#5285/#5283/#5269/#5268/#5260, ~20 SSE/translator/combo fixes #5248/#5250/#5254/#5261/#5255/#5273/#5258, M365 Copilot provider #5302, public-origin centralization #5278). Trust-but-verify: measured 4103 locally via `npm run quality:collect` on the release tip INCLUDING my reconciliation commits (CHANGELOG + main merge + the 2 regression test fixes 165c823f5) — the test fixes add 0 `any`/warnings (health-autopilot added a NextRequest import + asserts; chat-pipeline changed one Accept string + a comment), so all +13 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.", "_rebaseline_2026_06_28_v3839_release": "4002->4090 (+88). v3.8.39 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's 40 commits — antigravity remote-login + quota-family #5203/#5180/#5193, compression CCR-retrieve + TOON encoder #5187/#5163, ~20 SSE/translator/responses fixes #5156/#5154/#5197/#5204/#5158/#5123/#5166, proxy/health hardening #5202/#5208/#5209/#5201 from @KooshaPari, combo quota-share/context-relay E2E tests #5179/#5168/#5195). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, README.md and these baselines — 0 production-code change, so all +88 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.", @@ -102,9 +103,10 @@ "_rebaseline_2026_06_28_v3839_release": "78.4 -> 77.5 (-0.9, beyond the 0.5 eps). v3.8.39 cycle drift surfaced ONLY on the release PR (i18n-ui-coverage does NOT run on PR->release fast-gates). The cycle added new UI strings (compression studio TOON A/B table, antigravity remote-login dashboard field, amber warning icon) to the en denominator faster than the 37 non-en locales were translated; those locales need `npm run i18n:run` with OMNIROUTE_TRANSLATION_API_KEY (unavailable locally) — same precedent as _rebaseline_2026_06_18_v3828_cycle_close + _quality_rebaseline_2026_06_20_ci_ratchet. Measured by CI collect-metrics (run 28317145160) = 77.5. My release-finalize tree changes no src/i18n/messages/*.json. Tightening is tracked as follow-up (run i18n:run with creds)." }, "deadExports": { - "value": 310, + "value": 225, "direction": "down", "dedicatedGate": true, + "_rebaseline_2026_06_30_v3842_deadcode_wave": "310 -> 225. Measured by `node scripts/check/check-dead-code.mjs` on the v3.8.42 tip after the JxnLexn dead-code (#5463/#5464/#5466) + duplication (#5471..#5500) wave landed: DEAD_EXPORTS=133 + DEAD_FILES=92 = 225. The stale 310 was the v3.8.38 release snapshot never ratcheted on PR->release fast-gates (check:dead-code runs only on ci.yml PR->main, not quality.yml). Tightening to the true measured value; release-time captain rebaselines up if parallel cycle merges add dead exports.", "_rebaseline_2026_06_27_v3838_release": "345->346 (+1). v3.8.38 cycle drift surfaced by the release-green pre-flight (Quality Ratchet does NOT run on PR->release fast-gates). Net +1 inherited from this cycle's feature/fix merges (new executors/providers, compression fidelity-gate module) minus #5138's removal of dead legacy store modules. Release-finalize working tree touches ONLY CHANGELOG.md + i18n mirrors + README + baselines — 0 production-code change. Structural cleanup tracked as debt.", "_rebaseline_2026_06_26_v3837_release": "343->345. v3.8.37 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings/complexity accrued unmeasured across this cycle's 76 commits — provider adds DGrid/Pioneer/xAI, headroom proxy lifecycle #4649, ~50 SSE/translator fixes, Engine Combos #5062). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, and these baselines — 0 production-code change, so all drift is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle." }, diff --git a/docs/getting-started/FREE-TIERS-GUIDE.md b/docs/getting-started/FREE-TIERS-GUIDE.md index a090c89701..c06ad1709e 100644 --- a/docs/getting-started/FREE-TIERS-GUIDE.md +++ b/docs/getting-started/FREE-TIERS-GUIDE.md @@ -23,7 +23,7 @@ These providers are **always free** with no limits: | **Kiro AI** | Claude Sonnet 4.5, Haiku 4.5, Opus 4.6 | 50 credits/month | No auth needed | | **OpenCode Free** | GPT-4o, Claude, Gemini | Unlimited | No auth needed | | **Pollinations** | GPT-5, Claude, Gemini, DeepSeek, Llama 4 | No key needed | No auth needed | -| **LongCat** | LongCat-Flash-Lite | 50M tokens/day | No auth needed | +| **LongCat** | LongCat-2.0 | 10M tokens (one-time) | API key + KYC | | **Cloudflare AI** | 50+ models | 10K neurons/day | No auth needed | | **Qwen** | Qwen3-coder-plus/flash/next | Unlimited | No auth needed | | **Qoder** | Kimi-K2, DeepSeek-R1, Qwen3-coder | Unlimited | No auth needed | @@ -66,7 +66,7 @@ Connect these 4 providers for **unlimited free AI**: 1. **Kiro AI** — 50 credits/month (Claude models) 2. **OpenCode Free** — Unlimited (GPT models) 3. **Pollinations** — No key needed (multiple models) -4. **LongCat** — 50M tokens/day (backup) +4. **LongCat** — 10M tokens one-time (backup, requires KYC) Then use `model: "auto"` and OmniRoute will: - Try Kiro first (best quality) @@ -98,7 +98,7 @@ Browse the list and select one of these free providers: - **Kiro AI** — Free Claude models - **OpenCode Free** — Free GPT models - **Pollinations** — Free GPT-5, Claude, Gemini -- **LongCat** — 50M tokens/day free +- **LongCat** — 10M tokens free (one-time, requires KYC) - **Cloudflare AI** — 50+ models, 10K neurons/day ### Step 5: Click Connect @@ -136,10 +136,10 @@ Connect 3-4 free providers for the best experience. ### LongCat -- **Models**: LongCat-Flash-Lite -- **Quota**: 50M tokens/day -- **Auth**: No auth needed -- **Best for**: High-volume usage +- **Models**: LongCat-2.0 +- **Quota**: 10M tokens, one-time grant on signup (not recurring daily/monthly) +- **Auth**: API key + KYC verification required to unlock the free grant +- **Best for**: A one-off free allowance; pay-as-you-go beyond it ### Cloudflare AI @@ -222,7 +222,7 @@ Let's calculate how much free AI you can get: | Kiro AI | ~1.7 credits | 50 credits | | OpenCode Free | Unlimited | Unlimited | | Pollinations | Unlimited | Unlimited | -| LongCat | 50M tokens | 1.5B tokens | +| LongCat | — (one-time) | 10M tokens (one-time, KYC) | | Cloudflare AI | 10K neurons | 300K neurons | | NVIDIA NIM | ~40 RPM | ~1.7M requests | | Cerebras | 1M tokens | 30M tokens | diff --git a/docs/getting-started/PROVIDERS-GUIDE.md b/docs/getting-started/PROVIDERS-GUIDE.md index 7b7138d61f..94ad92502a 100644 --- a/docs/getting-started/PROVIDERS-GUIDE.md +++ b/docs/getting-started/PROVIDERS-GUIDE.md @@ -29,7 +29,7 @@ Think of a provider like a **phone carrier**. Just as you need a phone carrier t - **Kiro AI** — Free Claude models (no auth needed) - **OpenCode Free** — Free GPT models (no auth needed) - **Pollinations** — Free GPT-5, Claude, Gemini (no key needed) - - **LongCat** — 50M tokens/day free + - **LongCat** — 10M tokens free (one-time grant, requires account + KYC) - **Cloudflare AI** — 50+ models, 10K neurons/day 4. Click **Connect** 5. Done! You now have free AI access. @@ -69,7 +69,7 @@ These providers offer **free access** with no credit card: | **Kiro AI** | 50 credits/month | Claude Sonnet 4.5, Haiku 4.5, Opus 4.6 | No auth needed | | **OpenCode Free** | Unlimited | GPT-4o, Claude, Gemini | No auth needed | | **Pollinations** | No key needed | GPT-5, Claude, Gemini, DeepSeek, Llama 4 | No auth needed | -| **LongCat** | 50M tokens/day | LongCat-Flash-Lite | No auth needed | +| **LongCat** | 10M one-time | LongCat-2.0 | API key + KYC | | **Cloudflare AI** | 10K neurons/day | 50+ models | No auth needed | | **NVIDIA NIM** | ~40 RPM | 129 models | API key needed | | **Cerebras** | 1M tokens/day | Qwen3 235B, GPT-OSS 120B | API key needed | diff --git a/docs/i18n/ar/CHANGELOG.md b/docs/i18n/ar/CHANGELOG.md index e22065efeb..b92249b937 100644 --- a/docs/i18n/ar/CHANGELOG.md +++ b/docs/i18n/ar/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/az/CHANGELOG.md b/docs/i18n/az/CHANGELOG.md index ecc176e5b9..d1f39b7f0c 100644 --- a/docs/i18n/az/CHANGELOG.md +++ b/docs/i18n/az/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/bg/CHANGELOG.md b/docs/i18n/bg/CHANGELOG.md index ecc176e5b9..d1f39b7f0c 100644 --- a/docs/i18n/bg/CHANGELOG.md +++ b/docs/i18n/bg/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/bn/CHANGELOG.md b/docs/i18n/bn/CHANGELOG.md index 89392fec20..9f69b33761 100644 --- a/docs/i18n/bn/CHANGELOG.md +++ b/docs/i18n/bn/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/cs/CHANGELOG.md b/docs/i18n/cs/CHANGELOG.md index cf020753f9..527b6939f9 100644 --- a/docs/i18n/cs/CHANGELOG.md +++ b/docs/i18n/cs/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/da/CHANGELOG.md b/docs/i18n/da/CHANGELOG.md index 5920fac2cd..d0adabf3da 100644 --- a/docs/i18n/da/CHANGELOG.md +++ b/docs/i18n/da/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/de/CHANGELOG.md b/docs/i18n/de/CHANGELOG.md index d683af421a..2560adec57 100644 --- a/docs/i18n/de/CHANGELOG.md +++ b/docs/i18n/de/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/es/CHANGELOG.md b/docs/i18n/es/CHANGELOG.md index 41f43f1b47..aa4daceaf2 100644 --- a/docs/i18n/es/CHANGELOG.md +++ b/docs/i18n/es/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/fa/CHANGELOG.md b/docs/i18n/fa/CHANGELOG.md index 3a25927103..3cfc0b5a6b 100644 --- a/docs/i18n/fa/CHANGELOG.md +++ b/docs/i18n/fa/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/fi/CHANGELOG.md b/docs/i18n/fi/CHANGELOG.md index 76f7b06ee8..cd8a8e353f 100644 --- a/docs/i18n/fi/CHANGELOG.md +++ b/docs/i18n/fi/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/fr/CHANGELOG.md b/docs/i18n/fr/CHANGELOG.md index 1d6839537f..35362bff09 100644 --- a/docs/i18n/fr/CHANGELOG.md +++ b/docs/i18n/fr/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/gu/CHANGELOG.md b/docs/i18n/gu/CHANGELOG.md index 0d8478e3a9..51c1cc5991 100644 --- a/docs/i18n/gu/CHANGELOG.md +++ b/docs/i18n/gu/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/he/CHANGELOG.md b/docs/i18n/he/CHANGELOG.md index 05516caf8f..8235aa1cfa 100644 --- a/docs/i18n/he/CHANGELOG.md +++ b/docs/i18n/he/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/hi/CHANGELOG.md b/docs/i18n/hi/CHANGELOG.md index 4c0f703600..8195dafa29 100644 --- a/docs/i18n/hi/CHANGELOG.md +++ b/docs/i18n/hi/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/hu/CHANGELOG.md b/docs/i18n/hu/CHANGELOG.md index 8a87ba1904..2accdab046 100644 --- a/docs/i18n/hu/CHANGELOG.md +++ b/docs/i18n/hu/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/id/CHANGELOG.md b/docs/i18n/id/CHANGELOG.md index f7587dca58..304e3c180f 100644 --- a/docs/i18n/id/CHANGELOG.md +++ b/docs/i18n/id/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/in/CHANGELOG.md b/docs/i18n/in/CHANGELOG.md index b97ecec8c5..f3f84fa09c 100644 --- a/docs/i18n/in/CHANGELOG.md +++ b/docs/i18n/in/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/it/CHANGELOG.md b/docs/i18n/it/CHANGELOG.md index 0ef8606a5f..40f79e3604 100644 --- a/docs/i18n/it/CHANGELOG.md +++ b/docs/i18n/it/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/ja/CHANGELOG.md b/docs/i18n/ja/CHANGELOG.md index 9d91f2bd1f..90dce1964c 100644 --- a/docs/i18n/ja/CHANGELOG.md +++ b/docs/i18n/ja/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/ko/CHANGELOG.md b/docs/i18n/ko/CHANGELOG.md index c340b15326..9790e00a1e 100644 --- a/docs/i18n/ko/CHANGELOG.md +++ b/docs/i18n/ko/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/mr/CHANGELOG.md b/docs/i18n/mr/CHANGELOG.md index dcf5606199..d0dff86c88 100644 --- a/docs/i18n/mr/CHANGELOG.md +++ b/docs/i18n/mr/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/ms/CHANGELOG.md b/docs/i18n/ms/CHANGELOG.md index 9b1748c258..b296700c27 100644 --- a/docs/i18n/ms/CHANGELOG.md +++ b/docs/i18n/ms/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/nl/CHANGELOG.md b/docs/i18n/nl/CHANGELOG.md index 2e901bb2e2..e5c182a312 100644 --- a/docs/i18n/nl/CHANGELOG.md +++ b/docs/i18n/nl/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/no/CHANGELOG.md b/docs/i18n/no/CHANGELOG.md index 835093d171..f9ba0c569c 100644 --- a/docs/i18n/no/CHANGELOG.md +++ b/docs/i18n/no/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/phi/CHANGELOG.md b/docs/i18n/phi/CHANGELOG.md index 8dd1422cc5..f1cd2a45ec 100644 --- a/docs/i18n/phi/CHANGELOG.md +++ b/docs/i18n/phi/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/pl/CHANGELOG.md b/docs/i18n/pl/CHANGELOG.md index d60439272e..1df54da6d4 100644 --- a/docs/i18n/pl/CHANGELOG.md +++ b/docs/i18n/pl/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/pt-BR/CHANGELOG.md b/docs/i18n/pt-BR/CHANGELOG.md index 5fac560308..37354af189 100644 --- a/docs/i18n/pt-BR/CHANGELOG.md +++ b/docs/i18n/pt-BR/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/pt/CHANGELOG.md b/docs/i18n/pt/CHANGELOG.md index b5d16e5bca..dd96273225 100644 --- a/docs/i18n/pt/CHANGELOG.md +++ b/docs/i18n/pt/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/ro/CHANGELOG.md b/docs/i18n/ro/CHANGELOG.md index 33006a6a12..76c0d3b7ae 100644 --- a/docs/i18n/ro/CHANGELOG.md +++ b/docs/i18n/ro/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/ru/CHANGELOG.md b/docs/i18n/ru/CHANGELOG.md index b5c053a326..28ddb1c3ee 100644 --- a/docs/i18n/ru/CHANGELOG.md +++ b/docs/i18n/ru/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/sk/CHANGELOG.md b/docs/i18n/sk/CHANGELOG.md index 90acb8890f..8612764e0d 100644 --- a/docs/i18n/sk/CHANGELOG.md +++ b/docs/i18n/sk/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/sv/CHANGELOG.md b/docs/i18n/sv/CHANGELOG.md index e240458dde..0038bff5d1 100644 --- a/docs/i18n/sv/CHANGELOG.md +++ b/docs/i18n/sv/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/sw/CHANGELOG.md b/docs/i18n/sw/CHANGELOG.md index 9b6646f53c..a76d1df253 100644 --- a/docs/i18n/sw/CHANGELOG.md +++ b/docs/i18n/sw/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/ta/CHANGELOG.md b/docs/i18n/ta/CHANGELOG.md index e276f3de96..96d9c0f353 100644 --- a/docs/i18n/ta/CHANGELOG.md +++ b/docs/i18n/ta/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/te/CHANGELOG.md b/docs/i18n/te/CHANGELOG.md index bd39d91672..38b63d0937 100644 --- a/docs/i18n/te/CHANGELOG.md +++ b/docs/i18n/te/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/th/CHANGELOG.md b/docs/i18n/th/CHANGELOG.md index e4fc1fb956..e858cdb112 100644 --- a/docs/i18n/th/CHANGELOG.md +++ b/docs/i18n/th/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/tr/CHANGELOG.md b/docs/i18n/tr/CHANGELOG.md index cd298b065b..e78b7b2dfd 100644 --- a/docs/i18n/tr/CHANGELOG.md +++ b/docs/i18n/tr/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/uk-UA/CHANGELOG.md b/docs/i18n/uk-UA/CHANGELOG.md index c792dc6c0f..bdd8ac052b 100644 --- a/docs/i18n/uk-UA/CHANGELOG.md +++ b/docs/i18n/uk-UA/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/ur/CHANGELOG.md b/docs/i18n/ur/CHANGELOG.md index 7d2198d032..87e2f8792c 100644 --- a/docs/i18n/ur/CHANGELOG.md +++ b/docs/i18n/ur/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/vi/CHANGELOG.md b/docs/i18n/vi/CHANGELOG.md index a54bc6d3b4..878000619a 100644 --- a/docs/i18n/vi/CHANGELOG.md +++ b/docs/i18n/vi/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/zh-CN/CHANGELOG.md b/docs/i18n/zh-CN/CHANGELOG.md index a163cd287b..c7bae62a6f 100644 --- a/docs/i18n/zh-CN/CHANGELOG.md +++ b/docs/i18n/zh-CN/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/i18n/zh-TW/CHANGELOG.md b/docs/i18n/zh-TW/CHANGELOG.md index efd3574289..540e28da43 100644 --- a/docs/i18n/zh-TW/CHANGELOG.md +++ b/docs/i18n/zh-TW/CHANGELOG.md @@ -6,6 +6,98 @@ ## [3.8.31] — 2026-06-20 +## [3.8.42] — 2026-06-30 + +### ✨ New Features + +- **compression (pipeline):** add an honest default-on **inflation guard** to the stacked compression pipeline (T02 / Headroom H1). If the fully-stacked engines produce a body that did not actually shrink — its token count is `>=` the original — the compressed body is discarded and the verbatim original request is sent upstream instead, with a `pipeline-inflation-guard` warning recorded in the compression stats. This is safe by construction (the only fallback is the unmodified original, always a valid payload) and complements the existing opt-in per-step TV1 bail-out, which governs step-to-step advancement rather than the final output. New `open-sse/services/compression/pipelineGuards.ts`; wired at the single `finalizeStackedResult` choke point shared by the sync and async stacked paths. Regression guards (incl. an inflating-engine integration test) in `tests/unit/compression-pipeline-inflation-guard.test.ts`. + +- **compression (caveman):** complete the German, French, and Japanese rule packs with the `dedup` (repeated-context collapsing) and `ultra` (abbreviation / terse) categories they were missing — these three languages previously shipped only `context`/`filler`/`structural`, while `en`/`es`/`id`/`pt-BR` had all five. So a de/fr/ja conversation compressed at higher intensities now collapses repeated boilerplate ("wie bereits besprochen" → "Siehe oben.", "comme mentionné précédemment" → "Voir ci-dessus.", "前述のとおり" → "(上記参照)") and abbreviates dense technical vocabulary (`Datenbank`→`DB`, `Authentifizierung`→`Auth`; `base de données`→`BD`, `authentification`→`auth`; `データベース`→`DB`, `アプリケーション`→`app`). Patterns mirror the existing `es` pack and stay ReDoS-safe (bounded literal alternations; the CJK pack uses no `\b` since Japanese has no word boundaries). Regression guard: `tests/unit/caveman-packs-de-fr-ja.test.ts` (packs load + validate + shrink a representative sample). gaps v3.8.42 — T05/C2. + +- **compression (caveman):** add a **Chinese (zh / wenyan 文言) input-side rule pack** — the counterpart of the existing output-side `terse-cjk` style. New `rules/zh/{dedup,filler,ultra}.json` collapse repeated context ("如前所述" → "见上。"), drop pleasantries/hedging ("请帮我…/谢谢/我觉得"), strip sentence-final modal particles ("吗/呢/吧"), and abbreviate dense technical terms ("数据库"→"DB", "应用程序"→"app"). Chinese is now auto-detected: `detectCompressionLanguage` distinguishes zh from ja by Han-without-kana (kana is Japanese-exclusive, so a Han-heavy Japanese sentence still resolves to `ja`), and `zh` is listed in `listSupportedCompressionLanguages`. Patterns are ReDoS-safe (bounded literal alternations, no `\b` since CJK has no word boundaries). Regression guard: `tests/unit/caveman-packs-zh-wenyan.test.ts` (packs load + validate + shrink; zh/ja/non-CJK detection). gaps v3.8.42 — T05/C6. + +- **compression (RTK):** add **Gradle** and **.NET CLI (`dotnet`)** to the RTK tool-output filter catalog. Tool output for `gradle`/`gradlew` and `dotnet build|test|restore|publish` is now recognized (both by command and by output content) and compressed: Gradle daemon/welcome banners and no-op `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` lines are dropped while `BUILD SUCCESSFUL/FAILED`, "What went wrong", and stack traces are preserved; the .NET build banner, copyright, and `Determining projects to restore`/`Restored …` chatter are dropped while `Build succeeded/FAILED`, `error CS####`/`warning CS####`, and test summaries are preserved. New builtin filters `engines/rtk/filters/{gradle,dotnet}.json` (with inline tests run by the catalog gate) plus `gradle`/`dotnet` entries in the command detector. Regression guard: `tests/unit/rtk-gradle-dotnet-filters.test.ts`. gaps v3.8.42 — T07/R9. + +### 🔧 Bug Fixes + +- **providers (chatgpt-web):** fix `502 ChatGPT sentinel failed: Digest method not supported` on the **Electron desktop app**, which made every `chatgpt-web/*` request fail. The sentinel proof-of-work hashed with native `createHash("sha3-512")`, but Electron's Node is built against **BoringSSL, which does not implement the SHA-3 family** (electron/electron#30530), so the digest threw at construction — the provider was unusable on the desktop build (works under plain Node/OpenSSL). The PoW now hashes through a new runtime-portable helper (`open-sse/utils/sha3-512.ts`) that prefers the native digest and transparently falls back to a dependency-free pure-JS Keccak-f[1600] when native SHA-3 is absent. The fallback is validated bit-for-bit against native `createHash("sha3-512")` (300 random inputs) and the published FIPS-202 known-answer vectors. Regression guards in `tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`. ([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) + +- **providers (bytez):** fix Bytez key validation ("Provider validation endpoint not supported") and the chat base URL, verified live with a real key. Bytez **is** OpenAI-compatible at `…/models/v2/openai/v1`, but the registry stored the bare `…/models/v2` base, so the validation chat-probe hit `…/models/v2/chat/completions` → `404` → the misleading "endpoint not supported". Two parts: (1) the registry `baseUrl` now carries the full OpenAI-compat chat path (`…/models/v2/openai/v1/chat/completions`); (2) key validation no longer uses a chat probe — a Bytez account only serves models explicitly added to its catalog, so even valid keys 404 on any model id. A dedicated `validateBytezProvider` instead probes the **auth-only** `GET …/models/v2/list/tasks` endpoint (`200` ⇒ valid, `401/403` ⇒ invalid), which is independent of catalog provisioning. Regression guard: `tests/unit/bytez-validation-5422.test.ts`. ([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) + +- **dashboard (provider add):** two provider-add UX fixes. (1) #5420 — the "Import Models" button now stays hidden for **tool-only providers** (web search / web fetch), not just `*-search` ids: `firecrawl` and `jina-reader` (declared `serviceKinds: ["webFetch"]`) previously showed an Import button that hit the `400 "does not support models listing"` route. A new capability check (`providerLacksModelListing` over the resolved serviceKinds) gates the section without ever hiding an LLM/media provider. (2) #5426 — Coze key validation no longer leaks the raw upstream envelope (`{code,msg,logId,from}`) into the UI; the Coze-shaped error becomes a friendly `Coze rejected the key: (code )` message (scoped to `provider === "coze"` so no other provider is affected). Regression guards: `tests/unit/model-listing-capability-5420.test.ts`, `tests/unit/coze-validation-error-5426.test.ts`. ([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) + +- **providers (friendliai, novita):** fix two provider registry endpoints that rejected valid keys (verified live with real keys). **FriendliAI** pointed at `…/dedicated/v1/chat/completions`, which `403 Forbidden`s a serverless `flp_*` token — switched to `…/serverless/v1/chat/completions` (+ a serverless `modelsUrl`). **Novita** pointed at the legacy `…/v3/…` base with a typo'd model id `ai-ai/llama-3.1-8b-instruct` (both `404`) — switched to the OpenAI-compatible `…/openai/v1/…` base + the valid `meta-llama/llama-3.1-8b-instruct` id. Regression guard: `tests/unit/provider-endpoints-friendliai-novita.test.ts`. ([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) + +- **providers (muse-spark):** align the Muse Spark Web (Meta AI) cookie copy with the live cookie name. The default session cookie migrated from the retired `abra_sess` to `ecto_1_sess` (`META_AI_DEFAULT_COOKIE`), but the provider form hint and one 401 auth-failure message still told users to paste `abra_sess` — a cookie that no longer exists. Both strings now name `ecto_1_sess`. Regression guard: `tests/unit/muse-spark-cookie-copy-5449.test.ts`. ([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) + +- **dashboard (provider add):** fix three rough edges in the Add-API-Key / model-import flow reported across the provider-catalog audit. (1) The **Validation Model** and **Account ID** form fields shipped untranslated i18n stub copy (`"Validation Model Id Label"`, `"Account Id Placeholder"`, …) that surfaced verbatim in the modal — replaced with real labels/placeholders/hints in `en.json`. (2) Model import **silently fell back to the cached/local catalog**: the route already returned a `warning` ("API unavailable — using local catalog"), but `useModelImportHandlers` only read `models`/`error` and dropped it, so the user got local models with no indication — the warning is now surfaced as an import log line (new pure helper `extractImportWarning`). (3) The required connection-**name** field defaulted to `""`, which let browser autofill inject garbage (e.g. `wiw`) — it now defaults to `"main"`. Regression guard: `tests/unit/provider-add-ux-i18n-import-warning.test.ts`. ([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) + +- **services (installer):** fix `spawn EINVAL` when installing an embedded service (9Router / CLIProxy) on **Windows + Node.js 24+**. Node 24 stopped letting `child_process.execFile()` run `.cmd` batch files without a shell (nodejs/node#52554), and npm on Windows is `npm.cmd`, so `runNpm()` threw `EINVAL` the moment a user clicked **Install**. `runNpm` now enables `shell` on win32 only. To keep Hard Rule #13 intact under a shell — where the shell, not `execFile`, parses argv — the install `--prefix` (a `DATA_DIR` path that can legitimately contain spaces, e.g. `C:\Users\John Doe\.omniroute\…`) is now passed via the `npm_config_prefix` **environment variable** instead of an argv path, and the user-supplied install `version` is constrained to a dist-tag/semver shape (`SERVICE_VERSION_PATTERN`) at the route boundary so it can never carry shell metacharacters. With the prefix in the environment and the version validated, every remaining argv entry is a static flag. Regression guards: `tests/unit/services/installers/runNpm-shell-5379.test.ts` (+ existing `ninerouter.test.ts` aligned to npm's `npm_config_prefix` env). ([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) + +- **cli (serve):** restore `dist/tls-options.mjs` to the npm tarball — the opt-in native HTTPS/TLS sidecar (#5361) was copied into the staged `dist/` by the build but then **pruned** by the prepublish allowlist step, so `omniroute serve` crashed on the published 3.8.41 with `ERR_MODULE_NOT_FOUND` (`dist/server-ws.mjs` imports `./tls-options.mjs`). Added `tls-options.mjs` to `APP_STAGING_ALLOWED_EXACT_PATHS` (survives the prune) and `dist/tls-options.mjs` to `PACK_ARTIFACT_REQUIRED_PATHS` (the `check:pack-artifact` gate now fails loudly if it ever vanishes again — same guard pattern as `webdav-handler.mjs`). Regression guards in `tests/unit/pack-artifact-policy.test.ts`. ([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — thanks @KooshaPari for the parallel fix [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) + +- **dashboard:** fix the **Add Provider / onboarding wizard** button silently doing nothing. The `/dashboard/providers/new` route was a redirect stub (it bounced straight back to `/dashboard/providers`), so every "Add Provider" button and dashboard widget link opened nothing, and the fully-built `ProviderOnboardingWizard` component stayed orphaned (never rendered by any route). The route now renders the wizard directly; auth is enforced centrally by the `(dashboard)` layout, same as the sibling provider routes. Regression guard in `tests/unit/onboarding-wizard-route-5427.test.ts`. ([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) + +- **db (import):** fix `EBUSY: resource busy or locked` when importing a database on **Windows**. The import route deleted the live `storage.sqlite` + WAL/`-shm`/`-journal` sidecars with a plain `fs.unlinkSync` immediately after `resetDbInstance()`, but Windows releases the SQLite file handle asynchronously after `close()` (mmap / antivirus), so the unlink raced and threw `EBUSY`. The route now deletes via `unlinkFileWithRetry` (EBUSY/EPERM backoff) — the same helper the restore path already uses. Regression guard in `tests/unit/db-import-ebusy-5406.test.ts`. ([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406), consolidated under [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) + +- **build:** keep `ioredis` out of the client/CLI bundle — a dast-smoke regression revealed the module was being pulled into browser/Electron client-side chunks; adding it to the `SPAWN_CAPABLE_PREFIXES` leaf excludes it from client bundles while keeping it available on the server path. ([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) + +- **providers (mimocode):** route per-account traffic through SOCKS5 proxy dispatchers — each mimocode account's requests are now dispatched via its configured SOCKS5 proxy rather than the default direct connection. ([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — thanks @pizzav-xyz) + +- **providers:** persist the **Configured** provider filter selection across page reloads — the filter was resetting to "All" on every navigation. ([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — thanks @KooshaPari) + +- **providers (chatgpt-web):** support **GPT-5.5 Pro** model handoff — adds the model mapping and handoff routing needed for the GPT-5.5 Pro tier. ([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — thanks @Thinkscape) + +- **dashboard:** keep onboarding schemas browser-safe — the schema module imported a server-side `db` reference that crashed the browser bundle; it is now imported only on the server path. ([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — thanks @KooshaPari) + +- **routing (bifrost):** add auto-fallback cooldown for bifrost targets — prevents rapid re-selection of a failing bifrost backend within the cooldown window, complementing the existing circuit-breaker mechanism. ([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — thanks @KooshaPari) + +- **providers (opencode-plugin):** bump the opencode plugin to **v0.2.0** and wire auto-publish on release so the plugin package tracks OmniRoute releases automatically. ([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — thanks @herjarsa) + +- **rate-limit:** normalize queue refresh settings — aligns the queue-refresh interval configuration across rate-limit strategies so stale queues are released on a consistent schedule. ([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — thanks @KooshaPari) + +- **fallback:** normalize provider error-rule header extraction — ensures fallback retry decisions correctly read all response headers regardless of casing, fixing cases where a provider's `Retry-After` or custom error header was silently dropped. ([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — thanks @KooshaPari) + +- **routing:** gate Claude adaptive-thinking defaults behind the feature flag — prevents the thinking budget from being injected into requests for models that do not support the extended-thinking parameter, avoiding upstream `400` errors on non-thinking Claude variants. ([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — thanks @KooshaPari) + +- **ci:** fix post-merge CI regressions introduced by the dead-code sweep — restores test imports and type references broken when the ratchet landed before downstream consumers were updated. ([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — thanks @KooshaPari) + +- **sse:** treat terminal stream cancels as complete — an aborted SSE stream was being left in a partial state, causing downstream consumers to wait indefinitely for a final event that would never arrive. ([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — thanks @JxnLexn) + +- **api:** fix framing of non-streaming JSON responses — `stream: false` chat-completions responses were returned without correct content-length framing, causing some clients to misparse the response body. ([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — thanks @rdself) + +- **dashboard (tests):** protect dynamic dashboard endpoint tests with CSRF validation — the test suite was exercising dashboard API routes without CSRF tokens, masking a coverage gap for those endpoints. ([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — thanks @rdself) + +- **providers:** remove the dead **Phind** provider (service shut down) and deduplicate the **HuggingChat** catalog listing that had accumulated a stale duplicate entry. ([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — thanks @backryun) + +- **providers (longcat):** correct the LongCat free tier — **LongCat-2.0** is now GA; the one-time 10M-token promo (KYC required) is correctly reflected in the catalog, replacing the stale legacy beta entry. ([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — thanks @backryun) + +### 📝 Maintenance + +- **dashboard (refactor):** consolidate the duplicate caveman on/off toggle from the compression settings tab onto the single-source panel (T11), eliminating the stale off-sync copy. ([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) + +- **tests:** add quota guard for Claude-Code identity version lockstep (Phase 2) — asserts that the Claude-Code version reported in quota accounting stays in sync with the deployed version, preventing silent drift. ([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) + +- **docs:** add relay backend strategy guide documenting supported relay backend types, selection criteria, and configuration patterns. ([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) + +- **docs:** clarify bifrost relay backend environment variables — documents which env vars control bifrost's relay backend selection and failover behavior. ([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — thanks @KooshaPari) + +- **tests:** add relay routing fallback header behavior tests — regression guard asserting that fallback-triggered relay requests carry the correct forwarded headers through the routing layer. ([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — thanks @KooshaPari) + +- **ci:** add npm `fetch-retry` configuration and codify the release-freeze protocol (Hard Rule #21) — reduces transient npm registry fetch failures in CI and establishes the documented procedure for freezing releases. ([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) + +- **deps:** bump 11 production dependencies to their latest compatible versions. ([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) + +- **deps:** bump Electron from 42.4.1 to 42.5.1 in `/electron`. ([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) + +- **deps:** bump the development dependency group with 9 updates. ([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) + +- **maintenance (dead-code):** repo-wide sweep of unused exported symbols, types, and schemas — removes 35 no-longer-referenced exports across cloud-agent, a2a, SSE, memory, quota, skills, gamification, codex, qdrant, playground, provider catalog, and combo modules, reducing the exported API surface and eliminating stale misleading types. ([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400), [#5401](https://github.com/diegosouzapw/OmniRoute/pull/5401), [#5402](https://github.com/diegosouzapw/OmniRoute/pull/5402), [#5403](https://github.com/diegosouzapw/OmniRoute/pull/5403), [#5404](https://github.com/diegosouzapw/OmniRoute/pull/5404), [#5463](https://github.com/diegosouzapw/OmniRoute/pull/5463), [#5464](https://github.com/diegosouzapw/OmniRoute/pull/5464), [#5466](https://github.com/diegosouzapw/OmniRoute/pull/5466), [#5468](https://github.com/diegosouzapw/OmniRoute/pull/5468) — thanks @JxnLexn) + +- **maintenance (DRY):** DRY consolidation of shared helpers — extracts 17 duplicated utilities into single shared modules: vscode metadata helpers, proxy route handlers, auth zip extractors, combo-builder model options, vscode tokenized-request helpers, quota strategy ranking helpers, recharts donut card, provider-specific validation, batch response formatter, Redis runtime helpers, version-manager request parsing, media-generation route helpers, service install helpers, settings transform schemas, relay stream finalizer, machine-id fallback, and node SQLite adapter. ([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — thanks @JxnLexn) + +--- + ## [3.8.41] — 2026-06-29 ### ✨ New Features diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 607817ddd6..c4ed685575 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: OmniRoute API - version: 3.8.41 + version: 3.8.42 description: | OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible endpoint that routes requests to multiple AI providers with load balancing, diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index ee62da5052..55f5a2847e 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -261,6 +261,8 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp | `OMNIROUTE_TRUST_PROXY` | _(unset)_ | `src/server/origin/publicOrigin.ts` | Optional trust mode for forwarded public-origin headers. Unset = do not trust `Forwarded` / `X-Forwarded-*` for security decisions. `true` / `loopback` trusts forwarded host/proto only from a token-stamped loopback proxy. `private` / `lan` also trusts private-LAN proxy peers. Prefer explicit `NEXT_PUBLIC_BASE_URL` in production. | | `OMNIROUTE_CGPT_WEB_IMAGE_TIMEOUT_MS` | `180000` (3 min) | `open-sse/executors/chatgpt-web.ts` | Max wait time for an async chatgpt-web image to land via the celsius WebSocket. Increase during upstream queue-deep windows. | | `OMNIROUTE_CGPT_WEB_IMAGE_CACHE_MAX_MB` | `256` | `open-sse/services/chatgptImageCache.ts` | Total in-memory byte budget (MB) for the chatgpt-web image cache serving `/v1/chatgpt-web/image/`. Lower on memory-constrained hosts; raise if image generation is heavy and clients race the 30-minute TTL. | +| `OMNIROUTE_CGPT_WEB_PRO_TIMEOUT_MS` | `1200000` (20 min) | `open-sse/executors/chatgpt-web.ts` | Overall wait budget for a chatgpt-web GPT-5.5 Pro background-poll handoff. Pro reasoning runs complete out-of-band, so OmniRoute polls until the answer lands or this budget elapses. Raise if Pro requests time out before finishing. | +| `OMNIROUTE_CGPT_WEB_PRO_POLL_INTERVAL_MS` | `4000` (4s) | `open-sse/executors/chatgpt-web.ts` | Interval between chatgpt-web GPT-5.5 Pro background-poll attempts. Lower for snappier completion at the cost of more upstream polling; raise to reduce request volume. | | `THEOLDLLM_NAV_TIMEOUT_MS` | `30000` (30s) | `open-sse/executors/theoldllm.ts` | Playwright navigation timeout (ms) for the browser-backed token capture used by the The Old LLM (theoldllm) free provider. Raise on slow networks if the relay page is slow to settle. | | `KIE_CALLBACK_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Public callback URL for asynchronous kie.ai jobs. Highest-priority override before `OMNIROUTE_KIE_CALLBACK_URL` and `OMNIROUTE_PUBLIC_URL`. | | `OMNIROUTE_KIE_CALLBACK_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Alternate spelling of `KIE_CALLBACK_URL`. Falls back when the primary variable is unset. | @@ -597,6 +599,7 @@ REQUEST_TIMEOUT_MS (global override) | `OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS` | `120000` | Fallback used by `src/shared/utils/fetchTimeout.ts` when `FETCH_TIMEOUT_MS` is unset. | | `OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS` | `60000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`chatgptTlsClient.ts`). | | `OMNIROUTE_CHATGPT_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. | +| `OMNIROUTE_CHATGPT_STREAM_FIRST_BYTE_TIMEOUT_MS` | `30000` (30s) | Max wait for the first streamed byte from the ChatGPT TLS sidecar (`chatgptTlsClient.ts`) before aborting a dead stream. Raise if upstream cold-starts exceed the window. | | `OMNIROUTE_CLAUDE_TLS_TIMEOUT_MS` | `60000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`claudeTlsClient.ts`). | | `OMNIROUTE_CLAUDE_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. | | `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` | `30000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`perplexityTlsClient.ts`). | @@ -1006,9 +1009,10 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy), | `BIFROST_API_KEY` | _(unset)_ | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | API key for the Bifrost gateway (sent as `Authorization: Bearer ...`). If unset, the route expects the request to carry a valid OmniRoute API key; this key is for gateway-side auth only. | | `BIFROST_STREAMING_ENABLED` | `true` | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | When true, the Bifrost sidecar route streams responses back via SSE through the gateway rather than the TS streaming executor. Set to `0` to force non-streaming JSON responses through the gateway. | | `BIFROST_TIMEOUT_MS` | `30000` | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | Per-request timeout when proxying to the Bifrost gateway (ms). On timeout the route returns the TS relay path via the `X-Bifrost-Fallback` header. | -| `OMNIROUTE_BIFROST_KEY` | _(unset)_ | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | Alias for `BIFROST_API_KEY` (used by scripts that read the env via `OMNIROUTE_*`). Falls back to `BIFROST_API_KEY` when unset. | +| `OMNIROUTE_BIFROST_KEY` | _(unset)_ | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | Alias for `BIFROST_API_KEY` (used by scripts that read the env via `OMNIROUTE_*`). `BIFROST_API_KEY` takes precedence when both are set. | | `OMNIROUTE_RELAY_BACKEND` | `ts` / `auto` | `src/app/api/v1/relay/chat/completions/routingBackend.ts` | Relay backend for `/api/v1/relay/chat/completions`: `ts \| bifrost \| auto`. `ts` = TypeScript relay (default when Bifrost unconfigured); `auto` selects Bifrost when `BIFROST_BASE_URL` is set and `BIFROST_ENABLED` ≠ `0`, with automatic TS fallback if the sidecar is unreachable; `bifrost` forces Bifrost (strict, no fallback). Auth/rate-limit/injection-guard/allowlist always run in the Next route first. Responses carry `X-Routing-Backend` / `X-Routing-Fallback`. | | `RELAY_ROUTING_BACKEND` | _(unset)_ | `src/app/api/v1/relay/chat/completions/routingBackend.ts` | Accepted alias for `OMNIROUTE_RELAY_BACKEND` (same `ts \| bifrost \| auto` values). `OMNIROUTE_RELAY_BACKEND` takes precedence when both are set. | +| `OMNIROUTE_BIFROST_FAILURE_COOLDOWN_MS` | `5000` | `src/app/api/v1/relay/chat/completions/bifrostCooldown.ts` | Cooldown (ms) after a Bifrost sidecar hop fails in `auto` mode before the relay re-attempts the sidecar; it routes straight to the TS path while the cooldown lasts, then probes again. `0` disables. Only applies when `OMNIROUTE_RELAY_BACKEND=auto`. | | `OMNIROUTE_TLS_CERT` | _(unset)_ | `bin/cli/commands/serve.mjs` | Path to a PEM TLS certificate to serve `omniroute serve` over HTTPS (equivalent to `--tls-cert`). Must be paired with `OMNIROUTE_TLS_KEY`; the standalone server then terminates TLS on the same listener (`wss://` works unchanged). Unset → plain HTTP. Providing only one of cert/key, or an unreadable path, logs a warning and stays HTTP. | | `OMNIROUTE_TLS_KEY` | _(unset)_ | `bin/cli/commands/serve.mjs` | Path to the PEM TLS private key for `omniroute serve` HTTPS (equivalent to `--tls-key`). Must be paired with `OMNIROUTE_TLS_CERT`. See `OMNIROUTE_TLS_CERT`. | | `OMNIROUTE_LOCAL_ENDPOINTS_ENABLED` | `0` | `src/lib/security/localEndpoints.ts` | Master switch for `/api/local/*` routes. When unset or `0`, all `/api/local/*` routes return 503 in production. Must be `1` in non-loopback deploys to enable the Redis launcher and similar 1-click local service starters. Belt-and-suspenders with `isLocalOnlyPath()` route-guard classification (`LOCAL_ONLY_API_PREFIXES` in `src/server/authz/routeGuard.ts`). | diff --git a/docs/reference/FREE_TIERS.md b/docs/reference/FREE_TIERS.md index 697546df42..5651dd658f 100644 --- a/docs/reference/FREE_TIERS.md +++ b/docs/reference/FREE_TIERS.md @@ -25,7 +25,7 @@ lastUpdated: 2026-06-28 > **Why this dropped from the previous ~1.94B.** The 2026-06-17 refresh is an honesty correction, not a loss: `gemini` is now pool-deduped (was inflated by counting each Flash variant separately, 462M → 60M), `cloudflare-ai` corrected to its real 10k-Neurons/day (122M → 30M), `doubao` reclassified as a one-time signup credit (not recurring), and shut-down tiers removed (`github-models` closed to new signups, `chutes`/`phind`/`kluster`/`glhf` discontinued). Partly offset by `llm7` (correct 5M/day → 150M) and new free providers (Kilo, OpenCode Zen, Z.AI GLM-Flash). -Biggest **documented** contributors: `mistral` 1.00B, `llm7` 150M, `longcat` 150M (LongCat-2.0), `groq` 117M, `gemini` 60M, `cerebras` 30M, `cloudflare-ai` 30M, `sambanova` 30M. +Biggest **documented** contributors: `mistral` 1.00B, `llm7` 150M, `groq` 117M, `gemini` 60M, `cerebras` 30M, `cloudflare-ai` 30M, `sambanova` 30M. (`longcat` is excluded — its 10M LongCat-2.0 grant is a one-time, KYC-gated signup credit, not a recurring monthly budget.) > ⚠️ The theoretical ceiling (~10B) is inflated by rate-limit-only providers with **no published token cap** (`tencent`, `siliconflow`, `nvidia`, `baidu`, `glm-cn`, `sparkdesk`) whose figures would be `RPM/TPM × 24/7 × 30d` — a theoretical maximum no single account will sustain. They are **excluded** from the defensible number (shown in the "permanently free, no cap" row instead). This is the same inflation that makes competitors' multi-billion claims unreliable. @@ -38,7 +38,7 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve - **Removed / no free tier (2026):** `chutes` (free tier ended 2026-03), `phind` (company shut down 2026-01), `kluster` (sunset 2026-06-09 → MITO), `glhf` (beta ended), `gitlawb` + `gitlawb-gmi` (MiMo free revoked 2026-05-24, Nemotron promo ended 2026-06 — re-verified 2026-06-18), `aimlapi` (free tier paused — re-verified 2026-06-18), `yi` (Yi-Light retired, pay-as-you-go — re-verified 2026-06-18), `theoldllm` / `featherless-ai` (no current free tier). `iflytek` / `sparkdesk` stay listed but carry a ToS-caution note (Spark Lite is free; the ToS restricts proxy/relay use). - **GitHub Models** — closed to **new** customers on 2026-06-16; existing accounts keep API/playground access, so it stays in the catalog with a note (not removed). - **Gemini** — `2.0 Flash` / `2.0 Flash-Lite` shut down 2026-06-01 and `2.5 Pro` left the free tier (2026-04); free tier is now **Flash-family only** (2.5/3/3.1/3.5 Flash + Gemma). The catalog now **pools** the Flash family (was inflated by counting each variant separately: 462M → 60M). -- **Corrected numbers:** `cloudflare-ai` 122M → **30M** (real 10k-Neurons/day), `doubao` reclassified as a one-time signup credit (not recurring), `llm7` 4M → **150M** (documented 5M tokens/day), `together` "-Free" endpoints discontinued → only the **$25** signup credit remains, `longcat` retired 6 legacy models → **LongCat-2.0-Preview** only. +- **Corrected numbers:** `cloudflare-ai` 122M → **30M** (real 10k-Neurons/day), `doubao` reclassified as a one-time signup credit (not recurring), `llm7` 4M → **150M** (documented 5M tokens/day), `together` "-Free" endpoints discontinued → only the **$25** signup credit remains, `longcat` Preview ended + Flash models retired → **LongCat-2.0** only, reclassified as a one-time **10M**-token signup credit (KYC-gated, not recurring). - **New free providers discovered:** ⭐ **Kilo Code** (`kilo-gateway` — rotating "Auto Free" set: NVIDIA Nemotron 3 family, StepFun, Poolside, Nex-N2-Pro), ⭐ **OpenCode Zen** (`opencode-zen` — 6 rotating free coding models), ⭐ **Z.AI / Zhipu** (`glm-cn` — GLM-4-Flash / 4.5-Flash / 4.7-Flash permanently free + 20M signup bonus), and `arcee-ai` Trinity Large Preview. - **New honest tiers** (see Methodology): a _permanently-free-but-uncapped_ category (real recurring access, no token cap to count) and a _deposit-unlock boost_ (OpenRouter $10 → +24M/mo), both surfaced **separately** so they never inflate the headline. @@ -159,7 +159,6 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve | `freemodel-dev` | unknown | The Terms of Service page (freemodel.dev/terms) returned only a header with no readable content via WebFetch; no clause… | | `gitlawb` | unknown | No ToS or acceptable-use policy found; proxy/resale restrictions unknown — assume caution for self-hosted proxy use. | | `liquid` | unknown | No hosted API exists to proxy; open-source model commercial use is free for orgs under $10M annual revenue. No self-hos… | -| `phind` | unknown | Service is discontinued; ToS is no longer relevant. No proxy use is possible. | | `theoldllm` | unknown | No terms of service document was found on the site; proxying, resale, or self-hosted use policy is entirely undocumente… | | `yi` | unknown | ToS not publicly accessible without login; no proxy/resale clauses could be reviewed. Self-hosted personal proxy use st… | | `comfyui` | ok | GPL-3.0 open-source license explicitly permits self-hosted personal proxy use; Comfy Org ToS confirms commercial use of… | @@ -177,7 +176,7 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve |---|---|---|---|---|---| | `mistral` | recurring | ~1.00B | — | caution | 5 | | `llm7` | recurring | ~150M | — | caution | 4 | -| `longcat` | recurring | ~150M | — | caution | 1 | +| `longcat` | one-time | — | 10M | caution | 1 | | `gemini` | recurring | ~60M | — | caution | 6 | | `cerebras` | recurring | ~30M | — | caution | 2 | | `cloudflare-ai` | recurring | ~30M | — | caution | 6 | @@ -297,7 +296,7 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve - **`kiro`** — Catalog shipped freeNote "(none)" — but Kiro has a documented, perpetual free tier of 50 credits/month. The free tier existed since Kiro's public launch (pricing formalized ~October 2025). This is a … - **`kluster`** — The $5 free credits on signup appears to still match. However, there is evidence of an additional permanent free tier (post-credit) with undocumented limits, which may represent an improvement over t… - **`llm7`** — Rate limits have increased from the shipped freeNote (20 RPM / 100 req/hr → 40 RPM / 200 req/hr). The "no signup required" claim is now outdated — a free token from token.llm7.io is now required (tho… -- **`longcat`** — Significant change from shipped freeNote of "(none)": the platform actively provides 5M free tokens/day on a recurring daily basis to all API users during the public beta. +- **`longcat`** — The public preview/beta ended and the Flash models were retired; only the GA `LongCat-2.0` remains. The free tier is a **one-time 10M-token grant** unlocked after account signup + **KYC verification** — it does **not** reset daily or monthly. Beyond the grant it is pay-as-you-go. - **`mistral`** — The shipped freeNote ("Free Experiment tier: rate-limited access to all models") is directionally correct but understated. Current reality adds specific documented limits: 2 RPM, 500K TPM, 1B tokens/… - **`monsterapi`** — The shipped freeNote says "Free credits for decentralized GPU inference" which is partially accurate — there are one-time trial credits on signup. However, the recurring free tier has 0 credits/month… - **`morph`** — The shipped freeNote mentions "250K credits/month" which matches the current credit allocation; however, the more significant constraint is 200 requests/month which was not captured in the original c… @@ -308,7 +307,7 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve - **`nvidia`** — The "40 RPM, 70+ models" rate limit element matches the catalog, but the freeNote framing as a simple dev-access tier undersells that the old one-time credit pool has been removed — access is now tru… - **`ollama-cloud`** — Our shipped freeNote is "(none)" — this is stale. Ollama Cloud launched a cloud inference product with a genuine free tier that provides light weekly GPU-time-based access to hosted open models. - **`openrouter`** — RPD tightened from 200 to 50 for zero-credit accounts (RPM unchanged at 20). The catalog note was accurate on RPM but overstated the RPD by 4x for the no-credits baseline tier. -- **`phind`** — Our shipped freeNote describes an active free chat/code-search service — but Phind is fully discontinued as of January 16, 2026. The catalog entry should be marked discontinued and removed from activ… +- **`phind`** — Phind shut down on January 16, 2026. The provider has now been **fully removed** from the catalog (registry, executor, and both the web-cookie and API-key catalog entries) — matching the dead-service-removal precedent (#5246 Gemini CLI). - **`pollinations`** — Partially matches — the "no API key required" claim is still true for anonymous access, but the catalog freeNote omits that: (1) rate limits do apply (interval throttle of ~1 req/6-15s for anonymous … - **`predibase`** — The shipped freeNote ($25 free trial credits, 30-day validity) still matches current documentation. However, the catalog omits the concurrent 20,000 tokens/day serverless rate limit that applies duri… - **`publicai`** — The shipped freeNote ("Free community inference tier") is broadly accurate but understates the specificity: the 20 RPM rate limit is now documented. No major tightening found; the service remains fre… diff --git a/docs/reference/PROVIDER_REFERENCE.md b/docs/reference/PROVIDER_REFERENCE.md index a7e3dc0f66..dbdee596c2 100644 --- a/docs/reference/PROVIDER_REFERENCE.md +++ b/docs/reference/PROVIDER_REFERENCE.md @@ -1,16 +1,16 @@ --- title: "Provider Reference" -version: 3.8.40 -lastUpdated: 2026-06-29 +version: 3.8.42 +lastUpdated: 2026-06-30 --- # Provider Reference > **Auto-generated** from `src/shared/constants/providers.ts` — do not edit by hand. > Regenerate with: `npm run gen:provider-reference` -> **Last generated:** 2026-06-29 +> **Last generated:** 2026-06-30 -Total providers: **237**. See category breakdown below. +Total providers: **236**. See category breakdown below. ## Categories @@ -56,7 +56,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each | `windsurf` | `ws` | Windsurf (Devin CLI) | OAuth | [link](https://windsurf.com) | In the Windsurf / VS Code IDE, open the command palette and run `Windsurf: Provide Auth Token` (or click the Jupyter "Get Windsurf Authentication Token" button), then copy the shown token and paste it here. Note: opening windsurf.com/show-auth-token directly only renders a "Redirecting" page — the IDE must initiate the flow (it adds a `?state=...` param) for the token to appear. | | `zed` | `zd` | Zed IDE | OAuth | [link](https://zed.dev) | Zed stores LLM provider credentials (OpenAI, Anthropic, Google, Mistral, xAI) in the OS keychain. Use the Import button below to discover and import them automatically. | -## Web Cookie Providers (24) +## Web Cookie Providers (23) | ID | Alias | Name | Tags | Website | Notes | |----|-------|------|------|---------|-------| @@ -75,9 +75,8 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each | `inner-ai` | `in-ai` | Inner.ai (Subscription) | Web cookie | [link](https://app.innerai.com) | Paste your token cookie and email separated by a space: open DevTools → Application → Cookies → .innerai.com, copy the token value, then append a space and your Inner.ai login email. Example: eyJhbG... user@example.com | | `kimi-web` | `kimi-web` | Kimi Web (Moonshot AI) | Web cookie | [link](https://kimi.moonshot.cn) | Paste your session cookie from kimi.moonshot.cn (DevTools → Application → Cookies) | | `lmarena` | `lma` | LMArena (Free) | Web cookie | [link](https://lmarena.ai) | Paste the full Cookie header from lmarena.ai (DevTools → Network → request → Cookie). The session is now split across arena-auth-prod-v1.0, .1, … — copy the whole header. Optional — works with free tier for basic comparisons. | -| `muse-spark-web` | `ms-web` | Muse Spark Web (Meta AI) | Web cookie | [link](https://www.meta.ai) | Paste your abra_sess value or full cookie header from meta.ai | +| `muse-spark-web` | `ms-web` | Muse Spark Web (Meta AI) | Web cookie | [link](https://www.meta.ai) | Paste your ecto_1_sess value or full cookie header from meta.ai | | `perplexity-web` | `pplx-web` | Perplexity Web (Pro/Max) | Web cookie | [link](https://www.perplexity.ai) | Paste your __Secure-next-auth.session-token cookie value from perplexity.ai | -| `phind` | `ph` | Phind (Free) | Web cookie | [link](https://www.phind.com) | ⚠️ **DEPRECATED.** Phind shut down its API (2026-01); the /api/chat endpoint no longer serves (sweep 2026-06-19). | | `poe-web` | `poe` | Poe Web (Subscription) | Web cookie | [link](https://poe.com) | Paste your p-b cookie value from poe.com (DevTools → Application → Cookies → p-b) | | `qwen-web` | `qwen-web` | Qwen Web (Free) | Web cookie | [link](https://chat.qwen.ai) | Open chat.qwen.ai, log in, then open DevTools → Application → Local Storage → copy the "token" value (or use tongyi_sso_ticket cookie as Bearer token). | | `t3-web` | `t3chat` | t3.chat (Pro/Free) | Web cookie | [link](https://t3.chat) | Open t3.chat in your browser, log in, then open DevTools → Application → Local Storage → https://t3.chat. Copy the value of 'convex-session-id'. Also open DevTools → Network, copy the Cookie header from any request. Paste both values here. See provider setup docs for a step-by-step guide. | @@ -85,7 +84,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each | `venice-web` | `ven` | Venice Web (Privacy) | Web cookie | [link](https://venice.ai) | Paste your session cookie from venice.ai (DevTools → Application → Cookies) | | `zenmux-free` | `zmf` | ZenMux Free (Web) | Web cookie | [link](https://zenmux.ai) | Login at zenmux.ai, then export all cookies using EditThisCookie or Cookie-Editor and paste the full Cookie header string here. Refresh every ~30 days. | -## API Key Providers (paid / paid-with-free-credits) (160) +## API Key Providers (paid / paid-with-free-credits) (158) | ID | Alias | Name | Tags | Website | Notes | |----|-------|------|------|---------|-------| @@ -155,7 +154,6 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each | `hackclub` | `hc` | Hackclub AI | API key, aggregator | [link](https://ai.hackclub.com) | Sign in with your Hack Club account at ai.hackclub.com. | | `haiper` | `hp` | Haiper | API key, video | [link](https://haiper.ai) | Get API key at haiper.ai/haiper-api | | `heroku` | `heroku` | Heroku AI | API key, enterprise | [link](https://www.heroku.com) | — | -| `huggingchat` | `huggingchat` | HuggingChat | API key | [link](https://huggingface.co/chat) | No API key required for basic access. | | `huggingface` | `hf` | HuggingFace | API key | [link](https://huggingface.co) | Free Inference API for thousands of models (Whisper, VITS, SDXL…) | | `hyperbolic` | `hyp` | Hyperbolic | API key | [link](https://hyperbolic.xyz) | $1-5 trial credits on signup for serverless inference | | `ideogram` | `ideo` | Ideogram | API key | [link](https://ideogram.ai) | Get API key at ideogram.ai/docs/api | @@ -175,7 +173,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each | `liquid` | `liquid` | Liquid AI | API key | [link](https://liquid.ai) | Get API key at liquid.ai | | `llamagate` | `llamagate` | LlamaGate | API key | [link](https://llamagate.ai) | — | | `llm7` | `llm7` | LLM7.io | API key | [link](https://llm7.io) | No signup required - 2 req/s, 20 RPM, 100 req/hr free tier | -| `longcat` | `lc` | LongCat AI | API key | [link](https://longcat.chat/platform/docs) | Free: 5M tokens/day on LongCat-2.0-Preview (Flash models retired 2026-05-29); up to 120M/day via feedback. | +| `longcat` | `lc` | LongCat AI | API key | [link](https://longcat.chat/platform/docs) | Free: one-time 10M-token grant after account signup + KYC verification (LongCat-2.0). One-time only — not a recurring daily/monthly allowance. | | `maritalk` | `maritalk` | Maritalk | API key | [link](https://www.maritaca.ai) | — | | `meta-llama` | `meta` | Meta Llama API | API key | [link](https://llama.developer.meta.com) | — | | `minimax` | `minimax` | Minimax Coding | API key, video | [link](https://www.minimax.io) | — | @@ -203,7 +201,6 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each | `orcarouter` | `orcarouter` | OrcaRouter | API key | [link](https://www.orcarouter.ai) | — | | `ovhcloud` | `ovh` | OVHcloud AI | API key | [link](https://www.ovhcloud.com) | — | | `perplexity` | `pplx` | Perplexity | API key | [link](https://www.perplexity.ai) | — | -| `phind` | `phind` | Phind | API key | [link](https://phind.com) | Get API key at phind.com | | `piapi` | `pi` | PiAPI | API key, aggregator | [link](https://piapi.ai) | — | | `pioneer` | `pn` | Pioneer AI | API key | [link](https://pioneer.ai) | $75 free usage credits — no credit card required | | `poe` | `poe` | Poe | API key, aggregator | [link](https://creator.poe.com/api-reference) | Bearer API key for the Poe OpenAI-compatible API. | diff --git a/electron/package-lock.json b/electron/package-lock.json index 521a6ebb38..d3433a7923 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,18 +1,18 @@ { "name": "omniroute-desktop", - "version": "3.8.41", + "version": "3.8.42", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute-desktop", - "version": "3.8.41", + "version": "3.8.42", "license": "MIT", "dependencies": { "electron-updater": "^6.8.9" }, "devDependencies": { - "electron": "^42.4.1", + "electron": "^42.5.1", "electron-builder": "^26.15.3" }, "engines": { @@ -297,45 +297,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", - "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1130,15 +1091,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1415,9 +1367,9 @@ } }, "node_modules/electron": { - "version": "42.4.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-42.4.1.tgz", - "integrity": "sha512-8CYHJP5O4wFO+ycoJR98yy907MmPeo+vWXrzjxmGGgRNKqv8pOjjm+wphO0CCgQJnBU7+QUPSJS4QXhbKrO50w==", + "version": "42.5.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-42.5.1.tgz", + "integrity": "sha512-2VFNJcHHbrhIpGsJHdkLoi/nWPZPxN3GHVPe+9At3Oz3/TJRwpr+7JL97ddBDbKyLmHGx3GfI2jvzcEQL28uFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1459,19 +1411,6 @@ "node": ">=14.0.0" } }, - "node_modules/electron-builder-squirrel-windows": { - "version": "26.15.3", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", - "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "app-builder-lib": "26.15.3", - "builder-util": "26.15.3", - "electron-winstaller": "5.4.0" - } - }, "node_modules/electron-publish": { "version": "26.15.3", "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz", @@ -1506,66 +1445,6 @@ "tiny-typed-emitter": "^2.1.0" } }, - "node_modules/electron-winstaller": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", - "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@electron/asar": "^3.2.1", - "debug": "^4.1.1", - "fs-extra": "^7.0.1", - "lodash": "^4.17.21", - "temp": "^0.9.0" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "@electron/windows-sign": "^1.1.2" - } - }, - "node_modules/electron-winstaller/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/electron-winstaller/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "peer": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-winstaller/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2480,20 +2359,6 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2757,36 +2622,6 @@ "node": ">=18" } }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, "node_modules/proc-log": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", @@ -2981,21 +2816,6 @@ "node": ">= 4" } }, - "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -3251,21 +3071,6 @@ "node": ">=18" } }, - "node_modules/temp": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mkdirp": "^0.5.1", - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/temp-file": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", diff --git a/electron/package.json b/electron/package.json index 1720820298..ac49ce5b78 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "omniroute-desktop", - "version": "3.8.41", + "version": "3.8.42", "description": "OmniRoute Desktop Application", "main": "main.js", "author": { @@ -28,7 +28,7 @@ "electron-updater": "^6.8.9" }, "devDependencies": { - "electron": "^42.4.1", + "electron": "^42.5.1", "electron-builder": "^26.15.3" }, "overrides": { diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts index a25e1e363c..791eec8c43 100644 --- a/open-sse/config/freeModelCatalog.data.ts +++ b/open-sse/config/freeModelCatalog.data.ts @@ -241,7 +241,7 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [ { provider: "llm7", modelId: "gpt-4.1-nano-2025-04-14", displayName: "GPT-4.1 nano (LLM7)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-daily", poolKey: "llm7-free", tos: "caution" }, { provider: "llm7", modelId: "deepseek-r1-0528", displayName: "DeepSeek R1 (LLM7)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-daily", poolKey: "llm7-free", tos: "caution" }, { provider: "llm7", modelId: "qwen2.5-coder-32b-instruct", displayName: "Qwen2.5 Coder 32B (LLM7)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-daily", poolKey: "llm7-free", tos: "caution" }, - { provider: "longcat", modelId: "longcat-2.0-preview", displayName: "LongCat-2.0-Preview", monthlyTokens: 150000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "longcat-free", tos: "caution" }, + { provider: "longcat", modelId: "LongCat-2.0", displayName: "LongCat-2.0", monthlyTokens: 0, creditTokens: 10000000, freeType: "one-time-initial", poolKey: "longcat-free", tos: "caution" }, { provider: "mistral", modelId: "mistral-large-latest", displayName: "Mistral Large 3", monthlyTokens: 1000000000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "mistral", tos: "caution" }, { provider: "mistral", modelId: "mistral-medium-3-5", displayName: "Mistral Medium 3.5", monthlyTokens: 1000000000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "mistral", tos: "caution" }, { provider: "mistral", modelId: "mistral-small-latest", displayName: "Mistral Small 4", monthlyTokens: 1000000000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "mistral", tos: "caution" }, diff --git a/open-sse/config/freeTierCatalog.ts b/open-sse/config/freeTierCatalog.ts index 30038a48a8..48a08179fb 100644 --- a/open-sse/config/freeTierCatalog.ts +++ b/open-sse/config/freeTierCatalog.ts @@ -13,7 +13,6 @@ export type TosVerdict = "ok" | "caution" | "ambiguous" | "avoid" | "unknown"; export const FREE_TIER_BUDGETS: Record = { mistral: 1_000_000_000, - longcat: 150_000_000, "cloudflare-ai": 122_000_000, gemini: 60_000_000, doubao: 60_000_000, diff --git a/open-sse/config/providerErrorRules.ts b/open-sse/config/providerErrorRules.ts index d9f2797fe3..46b082b0c0 100644 --- a/open-sse/config/providerErrorRules.ts +++ b/open-sse/config/providerErrorRules.ts @@ -92,9 +92,7 @@ function buildMinimaxRules(): ProviderErrorRule[] { // If any model reports 0 remaining, the request was rejected for that // model. We classify as quota_exhausted so lockModel is called with // scope=model instead of poisoning the whole connection. - const exhausted = headerVal - .split(",") - .some((pair) => pair.split("=")[1]?.trim() === "0"); + const exhausted = headerVal.split(",").some((pair) => pair.split("=")[1]?.trim() === "0"); if (exhausted) { return { reason: "quota_exhausted", scope: "model" }; } @@ -129,7 +127,7 @@ export function getProviderErrorRuleMatch( body?: unknown ): ProviderErrorRuleMatch | null { if (!provider) return null; - const rules = providerRuleRegistry.get(provider); + const rules = providerRuleRegistry.get(provider.toLowerCase()); if (!rules) return null; // Normalize headers: accept either a `Headers` object (from `fetch()`) or // a plain record. Provider rules access headers via plain object indexing. @@ -137,7 +135,12 @@ export function getProviderErrorRuleMatch( ? {} : typeof (headers as Headers).get === "function" ? Object.fromEntries((headers as Headers).entries()) - : (headers as Record); + : Object.fromEntries( + Object.entries(headers as Record).map(([key, value]) => [ + key.toLowerCase(), + value, + ]) + ); for (const rule of rules) { const match = rule.match({ status, headers: safeHeaders, body }); if (match) return match; diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index 062572fb68..ac9847e86d 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -81,7 +81,6 @@ import { monsterapiProvider } from "./registry/monsterapi/index.ts"; import { sensenovaProvider } from "./registry/sensenova/index.ts"; import { hyperbolicProvider } from "./registry/hyperbolic/index.ts"; import { lambda_aiProvider } from "./registry/lambda-ai/index.ts"; -import { phindProvider } from "./registry/phind/index.ts"; import { t3_webProvider } from "./registry/t3-web/index.ts"; import { klusterProvider } from "./registry/kluster/index.ts"; import { iflytekProvider } from "./registry/iflytek/index.ts"; @@ -253,7 +252,6 @@ export const REGISTRY: Record = { sensenova: sensenovaProvider, hyperbolic: hyperbolicProvider, "lambda-ai": lambda_aiProvider, - phind: phindProvider, "t3-web": t3_webProvider, kluster: klusterProvider, iflytek: iflytekProvider, diff --git a/open-sse/config/providers/registry/bytez/index.ts b/open-sse/config/providers/registry/bytez/index.ts index a27fc432d5..80f6d46feb 100644 --- a/open-sse/config/providers/registry/bytez/index.ts +++ b/open-sse/config/providers/registry/bytez/index.ts @@ -6,7 +6,11 @@ export const bytezProvider: RegistryEntry = { alias: "bytez", format: "openai", executor: "default", - baseUrl: "https://api.bytez.com/models/v2", + // #5422: full OpenAI-compat chat URL. The bare `…/models/v2` base made the validation + // probe hit `…/models/v2/chat/completions` → 404 ("endpoint not supported"). Bytez is + // OpenAI-compatible at `…/models/v2/openai/v1`; store the full chat path (like + // friendliai/novita) so chat resolves once an account has catalog models. + baseUrl: "https://api.bytez.com/models/v2/openai/v1/chat/completions", authType: "apikey", authHeader: "bearer", models: CHAT_OPENAI_COMPAT_MODELS.bytez, diff --git a/open-sse/config/providers/registry/chatgpt-web/index.ts b/open-sse/config/providers/registry/chatgpt-web/index.ts index 79ecfb6004..1fcb470a9d 100644 --- a/open-sse/config/providers/registry/chatgpt-web/index.ts +++ b/open-sse/config/providers/registry/chatgpt-web/index.ts @@ -9,14 +9,15 @@ export const chatgpt_webProvider: RegistryEntry = { authType: "apikey", authHeader: "cookie", models: [ - { id: "gpt-5.5-pro", name: "GPT-5.5 Pro" }, //pro tier only - { id: "gpt-5.5-thinking", name: "GPT-5.5 Thinking" }, //plus, pro tier - { id: "gpt-5.5", name: "GPT-5.5 Instant" }, //free, plus, pro tier - { id: "gpt-5.4-pro", name: "GPT-5.4 Pro" }, //pro tier only - { id: "gpt-5.4-thinking", name: "GPT-5.4 Thinking" }, //plus, pro tier - { id: "gpt-5.4-thinking-mini", name: "GPT-5.4 Thinking Mini" }, //free-login only - { id: "gpt-5.3", name: "GPT-5.3 Instant" }, //free, free-login, plus, pro tier - { id: "gpt-5.3-mini", name: "GPT-5.3 Mini" }, //limit fallback - { id: "o3", name: "o3" }, //plus ~ tier + { id: "gpt-5-5-pro", name: "GPT-5.5 Pro" }, // pro tier only, standard effort + { id: "gpt-5-5-pro-extended", name: "GPT-5.5 Pro Extended" }, // pro tier only, extended effort + { id: "gpt-5-5-thinking", name: "GPT-5.5 Thinking" }, // plus, pro tier + { id: "gpt-5-5", name: "GPT-5.5 Instant" }, // free, plus, pro tier + { id: "gpt-5-4-pro", name: "GPT-5.4 Pro" }, // pro tier only + { id: "gpt-5-4-thinking", name: "GPT-5.4 Thinking" }, // plus, pro tier + { id: "gpt-5-4-t-mini", name: "GPT-5.4 Thinking Mini" }, // free-login only + { id: "gpt-5-3", name: "GPT-5.3 Instant" }, // free, free-login, plus, pro tier + { id: "gpt-5-3-mini", name: "GPT-5.3 Mini" }, // limit fallback + { id: "o3", name: "o3" }, // plus ~ tier ], }; diff --git a/open-sse/config/providers/registry/friendliai/index.ts b/open-sse/config/providers/registry/friendliai/index.ts index 95e0608bdb..d4dd98179f 100644 --- a/open-sse/config/providers/registry/friendliai/index.ts +++ b/open-sse/config/providers/registry/friendliai/index.ts @@ -6,7 +6,10 @@ export const friendliaiProvider: RegistryEntry = { alias: "friendli", format: "openai", executor: "default", - baseUrl: "https://api.friendli.ai/dedicated/v1/chat/completions", + // #5430: serverless endpoint — a `flp_*` serverless token gets 403 Forbidden on the + // /dedicated path (verified live). Serverless accepts it and serves the public models. + baseUrl: "https://api.friendli.ai/serverless/v1/chat/completions", + modelsUrl: "https://api.friendli.ai/serverless/v1/models", authType: "apikey", authHeader: "bearer", models: CHAT_OPENAI_COMPAT_MODELS.friendliai, diff --git a/open-sse/config/providers/registry/longcat/index.ts b/open-sse/config/providers/registry/longcat/index.ts index af1569d077..d53c2a32ef 100644 --- a/open-sse/config/providers/registry/longcat/index.ts +++ b/open-sse/config/providers/registry/longcat/index.ts @@ -9,12 +9,15 @@ export const longcatProvider: RegistryEntry = { authType: "apikey", authHeader: "Authorization", authPrefix: "Bearer", - // Sweep 2026-06-19: the LongCat-Flash-* line was officially retired 2026-05-29; the - // current docs (longcat.chat/platform/docs) expose only LongCat-2.0-Preview. + // Sweep 2026-06-30: the LongCat-Flash-* line was retired 2026-05-29 and the Preview + // ended; current docs (longcat.chat/platform/docs) expose only the GA LongCat-2.0 + // (1M context, 128K max output). Free tier is a ONE-TIME 10M-token grant unlocked + // after account signup + KYC verification — NOT a recurring daily/monthly allowance. + // Beyond the free quota it is pay-as-you-go (see providerCostData). models: [ { - id: "LongCat-2.0-Preview", - name: "LongCat 2.0 Preview (10M tok/day 🆓)", + id: "LongCat-2.0", + name: "LongCat 2.0 (10M tok free 🆓)", contextLength: 1048576, }, ], diff --git a/open-sse/config/providers/registry/novita/index.ts b/open-sse/config/providers/registry/novita/index.ts index 683ffd600a..cd57d24523 100644 --- a/open-sse/config/providers/registry/novita/index.ts +++ b/open-sse/config/providers/registry/novita/index.ts @@ -5,9 +5,11 @@ export const novitaProvider: RegistryEntry = { alias: "novita", format: "openai", executor: "default", - baseUrl: "https://api.novita.ai/v3/chat/completions", - modelsUrl: "https://api.novita.ai/v3/models", + // #5455: OpenAI-compatible endpoint. The legacy /v3 base + the `ai-ai/…` model id both + // 404 (verified live); /openai/v1 + the `meta-llama/…` id return a clean completion. + baseUrl: "https://api.novita.ai/openai/v1/chat/completions", + modelsUrl: "https://api.novita.ai/openai/v1/models", authType: "apikey", authHeader: "bearer", - models: [{ id: "ai-ai/llama-3.1-8b-instruct", name: "Llama 3.1 8B" }], + models: [{ id: "meta-llama/llama-3.1-8b-instruct", name: "Llama 3.1 8B Instruct" }], }; diff --git a/open-sse/config/providers/registry/phind/index.ts b/open-sse/config/providers/registry/phind/index.ts deleted file mode 100644 index 5ebb35c207..0000000000 --- a/open-sse/config/providers/registry/phind/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { RegistryEntry } from "../../shared.ts"; - -export const phindProvider: RegistryEntry = { - id: "phind", - alias: "ph", - format: "openai", - executor: "phind", - baseUrl: "https://www.phind.com/api/chat", - authType: "apikey", - authHeader: "cookie", - models: [ - { id: "phind-model", name: "Phind Model (Auto)" }, - { id: "gpt-4o", name: "GPT-4o (via Phind)" }, - { id: "claude-3.5-sonnet", name: "Claude 3.5 Sonnet (via Phind)" }, - ], -}; diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index 6795546ad4..45e2853179 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -1057,8 +1057,11 @@ export class BaseExecutor { delete tb.thinking; delete tb.context_management; appliedThinking = "off"; - } else if (!effThinking && !headerEffort) { - // Default CC logic when no override headers are present + } else if (!effThinking && !headerEffort && isClaudeCodeClient) { + // Default Claude Code logic when no override headers are present. + // Generic OpenAI-compatible clients that route through native Claude OAuth + // must opt in with x-omniroute-thinking; force-injecting adaptive thinking + // leaks non-standard reasoning replay fields back into those clients. const isHaiku = typeof tb.model === "string" && tb.model.includes("haiku"); // #5312 RC-B: honor the operator's proxy-level Thinking-Budget mode. // `auto` means "strip — let the provider decide", so suppress the default diff --git a/open-sse/executors/chatgpt-web.ts b/open-sse/executors/chatgpt-web.ts index 7760d444c1..135c9d450c 100644 --- a/open-sse/executors/chatgpt-web.ts +++ b/open-sse/executors/chatgpt-web.ts @@ -20,6 +20,7 @@ import { describeChatGptWebHttpError } from "./chatgptWebErrors.ts"; import { prepareToolMessages } from "../translator/webTools.ts"; import { buildToolModeResponse } from "./chatgptWebTools.ts"; import { createHash, randomUUID, randomBytes } from "node:crypto"; +import { sha3_512Hex } from "../utils/sha3-512.ts"; import { tlsFetchChatGpt, TlsClientUnavailableError, @@ -41,6 +42,9 @@ const SENTINEL_CR_URL = `${CHATGPT_BASE}/backend-api/sentinel/chat-requirements` const CONV_URL = `${CHATGPT_BASE}/backend-api/f/conversation`; const USER_LAST_USED_MODEL_CONFIG_URL = `${CHATGPT_BASE}/backend-api/settings/user_last_used_model_config`; +const DEFAULT_PRO_POLL_TIMEOUT_MS = 20 * 60_000; +const DEFAULT_PRO_POLL_INTERVAL_MS = 4_000; + const CHATGPT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:152.0) Gecko/20100101 Firefox/152.0"; @@ -81,7 +85,20 @@ function deviceIdFor(cookie: string): string { // /backend-api/models on a logged-in account; "gpt-5-4-t-mini" is ChatGPT's // abbreviated slug for "GPT-5.4 Thinking Mini". const MODEL_MAP: Record = { + // Current ChatGPT Web slugs (these are what the provider catalog exposes). + "gpt-5-5-pro": "gpt-5-5-pro", + "gpt-5-5-pro-extended": "gpt-5-5-pro", + "gpt-5-5-thinking": "gpt-5-5-thinking", + "gpt-5-5": "gpt-5-5", + "gpt-5-4-pro": "gpt-5-4-pro", + "gpt-5-4-thinking": "gpt-5-4-thinking", + "gpt-5-4-t-mini": "gpt-5-4-t-mini", + "gpt-5-3": "gpt-5-3", + "gpt-5-3-mini": "gpt-5-3-mini", + + // Legacy OmniRoute dot-form aliases kept for direct provider/model callers. "gpt-5.5-pro": "gpt-5-5-pro", + "gpt-5.5-pro-extended": "gpt-5-5-pro", "gpt-5.5-thinking": "gpt-5-5-thinking", "gpt-5.5": "gpt-5-5", "gpt-5.4-pro": "gpt-5-4-pro", @@ -93,6 +110,13 @@ const MODEL_MAP: Record = { o3: "o3", }; +const MODEL_FORCED_EFFORT: Record = { + "gpt-5-5-pro": "standard", + "gpt-5-5-pro-extended": "extended", + "gpt-5.5-pro": "standard", + "gpt-5.5-pro-extended": "extended", +}; + /** Set of chatgpt.com slugs that the user_last_used_model_config endpoint * accepts a `thinking_effort` value for, derived from MODEL_MAP so adding a * new thinking entry there automatically extends this set. Includes the @@ -514,6 +538,36 @@ function resolveThinkingEffort( return normalizeThinkingEffort(nested); } +interface ResolvedChatGptModel { + slug: string; + effort: "standard" | "extended" | null; + isPro: boolean; +} + +function resolveChatGptModel( + model: string, + body: unknown, + providerSpecificData: Record | undefined +): ResolvedChatGptModel { + const slug = MODEL_MAP[model] ?? model; + const forcedEffort = MODEL_FORCED_EFFORT[model] ?? null; + const effort = forcedEffort ?? resolveThinkingEffort(body, providerSpecificData); + const isPro = slug === "gpt-5-5-pro"; + return { slug, effort, isPro }; +} + +function configuredProPollTimeoutMs(): number { + const raw = Number(process.env.OMNIROUTE_CGPT_WEB_PRO_TIMEOUT_MS); + if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_PRO_POLL_TIMEOUT_MS; + return Math.floor(raw); +} + +function configuredProPollIntervalMs(): number { + const raw = Number(process.env.OMNIROUTE_CGPT_WEB_PRO_POLL_INTERVAL_MS); + if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_PRO_POLL_INTERVAL_MS; + return Math.floor(raw); +} + async function setUserThinkingEffort( modelSlug: string, effort: "standard" | "extended", @@ -832,9 +886,8 @@ async function solvePow(opts: PowOptions): Promise { cfg[3] = i; const json = JSON.stringify(cfg); const b64 = Buffer.from(json).toString("base64"); - const hash = createHash("sha3-512") - .update(opts.seed + b64) - .digest("hex"); + // Portable SHA3-512 — pure-JS fallback under Electron/BoringSSL (#5531). + const hash = sha3_512Hex(opts.seed + b64); if (opts.target && hash.slice(0, opts.target.length) <= opts.target) { return `${opts.prefix}${b64}`; } @@ -1055,11 +1108,14 @@ function buildConversationBody( parsed: ParsedMessages, modelSlug: string, parentMessageId: string, - // When true, send as a regular (non-temporary) chat so the image_gen tool - // is available. When false (default), use Temporary Chat to keep chats - // out of the user's chatgpt.com history. - forImageGen: boolean, - continuation: ChatGptImageConversationContext | null = null + options: { + // Keep text/API calls in Temporary Chat so they do not clutter the user's + // chatgpt.com history. Disable Temporary Chat only when ChatGPT needs a + // durable image conversation (image generation/editing). + persistConversation: boolean; + thinkingEffort: "standard" | "extended" | null; + continuation?: ChatGptImageConversationContext | null; + } ): Record { // Critical: do NOT send prior turns as separate `assistant` and `user` // messages in the `messages` array. ChatGPT's web API ("action: next") @@ -1074,6 +1130,8 @@ function buildConversationBody( if (parsed.systemMsg.trim()) { systemParts.push(parsed.systemMsg.trim()); } + const continuation = options.continuation ?? null; + if (!continuation && parsed.history.length > 0) { const formatted = parsed.history .map((h) => `${h.role === "assistant" ? "Assistant" : "User"}: ${h.content}`) @@ -1113,13 +1171,16 @@ function buildConversationBody( conversation_id: continuation?.conversationId ?? null, parent_message_id: continuation?.parentMessageId ?? parentMessageId, timezone_offset_min: -new Date().getTimezoneOffset(), - // Temporary Chat is the default. Disable it ONLY when the user is asking - // for an image — that lets ChatGPT use its image_gen tool, at the cost of - // saving the chat to the user's history. For text-only requests we keep - // Temporary Chat on so the user's history stays clean. - history_and_training_disabled: !(forImageGen || continuation), + // Temporary Chat is the default. Disable it only for image generation / + // image edits, where ChatGPT needs durable conversation state for tools. + history_and_training_disabled: !options.persistConversation, suggestions: [], websocket_request_id: randomUUID(), + conversation_mode: { kind: "primary_assistant" }, + supports_buffering: true, + force_parallel_switch: "auto", + paragen_cot_summary_display_override: "allow", + ...(options.thinkingEffort ? { thinking_effort: options.thinkingEffort } : {}), }; } @@ -1161,15 +1222,23 @@ async function* readChatGptSseEvents( const decoder = new TextDecoder(); let buffer = ""; let dataLines: string[] = []; + let eventName: string | null = null; function flush(): ChatGptStreamEvent | null | "done" { - if (dataLines.length === 0) return null; + if (dataLines.length === 0) { + eventName = null; + return null; + } const payload = dataLines.join("\n"); dataLines = []; + const sseEventName = eventName; + eventName = null; const trimmed = payload.trim(); if (!trimmed || trimmed === "[DONE]") return "done"; try { - return JSON.parse(trimmed) as ChatGptStreamEvent; + const parsed = JSON.parse(trimmed) as ChatGptStreamEvent; + if (sseEventName && !parsed.type) parsed.type = sseEventName; + return parsed; } catch { console.warn("[chatgpt-web] stream event JSON parse failed"); return null; @@ -1196,7 +1265,9 @@ async function* readChatGptSseEvents( if (parsed) yield parsed; continue; } - if (line.startsWith("data:")) { + if (line.startsWith("event:")) { + eventName = line.slice(6).trim(); + } else if (line.startsWith("data:")) { dataLines.push(line.slice(5).trimStart()); } } @@ -1234,6 +1305,8 @@ interface ContentChunk { * conversation endpoint for the actual image. */ imageGenAsync?: boolean; + /** True when ChatGPT handed the turn off to a long-running worker. */ + handoff?: boolean; } interface ImagePointerRef { @@ -1290,6 +1363,7 @@ async function* extractContent( // tool (see ContentChunk.imageGenAsync). The actual image arrives later via // WebSocket / polling — caller handles that. let imageGenAsync = false; + let handoff = false; for await (const event of readChatGptSseEvents(eventStream, signal)) { if (event.error) { @@ -1303,6 +1377,15 @@ async function* extractContent( if (event.conversation_id) conversationId = event.conversation_id; + if (event.type === "stream_handoff") { + handoff = true; + yield { + conversationId: conversationId ?? undefined, + handoff: true, + }; + continue; + } + // Detect image_gen on top-level "server_ste_metadata" events. These don't // have a `message` field so the post-message guard would skip them, but // they're the most reliable signal — `turn_use_case: "image gen"`. @@ -1407,10 +1490,208 @@ async function* extractContent( messageId: currentId ?? undefined, imagePointers: imagePointers.size > 0 ? Array.from(imagePointers.values()) : undefined, imageGenAsync, + handoff, done: true, }; } +// ─── Long-running Pro handoff polling ────────────────────────────────────── + +interface ChatGptDetailMessage { + id?: string; + author?: { role?: string }; + content?: { + content_type?: string; + parts?: unknown[]; + text?: string; + }; + status?: string; + end_turn?: boolean; + create_time?: number; + update_time?: number; + metadata?: Record; +} + +interface ChatGptConversationDetail { + mapping?: Record; +} + +interface FinalAssistantAnswer { + text: string; + messageId?: string; + finished: boolean; +} + +function textFromContentPart(part: unknown): string { + if (typeof part === "string") return part; + if (!part || typeof part !== "object") return ""; + const obj = part as Record; + for (const key of ["text", "content", "summary"]) { + const value = obj[key]; + if (typeof value === "string") return value; + } + return ""; +} + +function detailMessageText(message: ChatGptDetailMessage): string { + const content = message.content; + if (!content) return ""; + if (typeof content.text === "string") return content.text; + const parts = content.parts ?? []; + return parts.map(textFromContentPart).join(""); +} + +function extractFinalAssistantAnswer( + detail: ChatGptConversationDetail +): FinalAssistantAnswer | null { + const nodes = Object.values(detail.mapping ?? {}); + let best: (FinalAssistantAnswer & { sort: number }) | null = null; + + for (const node of nodes) { + const message = node.message; + if (!message || message.author?.role !== "assistant") continue; + if (message.metadata?.is_visually_hidden === true) continue; + const contentType = message.content?.content_type ?? ""; + if (contentType.includes("thought") || contentType.includes("reasoning")) continue; + + const text = detailMessageText(message).trim(); + if (!text) continue; + const finished = message.status === "finished_successfully" && message.end_turn !== false; + const sort = message.update_time ?? message.create_time ?? 0; + if ( + !best || + (finished && (!best.finished || sort >= best.sort)) || + (!finished && !best.finished && sort >= best.sort) + ) { + best = { text, messageId: message.id, finished, sort }; + } + } + + if (!best) return null; + return { text: best.text, messageId: best.messageId, finished: best.finished }; +} + +function delayWithAbort(ms: number, signal?: AbortSignal | null): Promise { + if (ms <= 0) return Promise.resolve(); + if (signal?.aborted) return Promise.resolve(); + return new Promise((resolve) => { + const timer = setTimeout(() => { + signal?.removeEventListener("abort", onAbort); + resolve(); + }, ms); + const onAbort = () => { + clearTimeout(timer); + signal?.removeEventListener("abort", onAbort); + resolve(); + }; + signal?.addEventListener("abort", onAbort, { once: true }); + }); +} + +function decodeUtf8DataUrl(text: string): string { + const marker = ";base64,"; + if (!text.startsWith("data:") || !text.includes(marker)) return text; + const base64 = text.slice(text.indexOf(marker) + marker.length); + return new TextDecoder().decode(Buffer.from(base64, "base64")); +} + +interface ConversationDetailFetchResult { + detail: ChatGptConversationDetail | null; + terminal: boolean; +} + +async function fetchConversationDetail( + conversationId: string, + ctx: ResolverContext +): Promise { + const url = `${CHATGPT_BASE}/backend-api/conversation/${encodeURIComponent(conversationId)}`; + const headers: Record = { + ...browserHeaders(), + ...oaiHeaders(ctx.sessionId, ctx.deviceId), + Accept: "application/json", + Authorization: `Bearer ${ctx.accessToken}`, + Cookie: buildSessionCookieHeader(ctx.cookie), + }; + if (ctx.accountId) headers["chatgpt-account-id"] = ctx.accountId; + + try { + const response = await tlsFetchChatGpt(url, { + method: "GET", + headers, + timeoutMs: 30_000, + signal: ctx.signal, + // The native tls-client text path can surface UTF-8 JSON as mojibake + // (e.g. 👉 becomes 👉). Ask for raw bytes and decode as UTF-8 here so + // the final answer appended after Pro stream_handoff preserves Unicode. + byteResponse: true, + }); + if (response.status >= 400) { + ctx.log?.warn?.( + "CGPT-WEB", + `conversation poll ${response.status}: ${(response.text || "").slice(0, 300)}` + ); + return { detail: null, terminal: [401, 403, 404].includes(response.status) }; + } + if (!response.text) return { detail: null, terminal: false }; + return { + detail: JSON.parse(decodeUtf8DataUrl(response.text)) as ChatGptConversationDetail, + terminal: false, + }; + } catch (err) { + ctx.log?.warn?.( + "CGPT-WEB", + `conversation poll failed: ${err instanceof Error ? err.message : String(err)}` + ); + return { detail: null, terminal: false }; + } +} + +async function pollForFinalAssistantAnswer( + conversationId: string, + ctx: ResolverContext +): Promise { + const started = Date.now(); + const timeoutMs = configuredProPollTimeoutMs(); + const intervalMs = configuredProPollIntervalMs(); + let last: FinalAssistantAnswer | null = null; + let terminalPollFailure = false; + + while (!ctx.signal?.aborted && Date.now() - started < timeoutMs) { + const { detail, terminal } = await fetchConversationDetail(conversationId, ctx); + if (detail) { + const answer = extractFinalAssistantAnswer(detail); + if (answer) { + last = answer; + if (answer.finished) return answer; + } + } + if (terminal) { + terminalPollFailure = true; + break; + } + const remaining = timeoutMs - (Date.now() - started); + if (remaining <= 0) break; + await delayWithAbort(Math.min(intervalMs, remaining), ctx.signal); + } + + if (last) { + ctx.log?.warn?.( + "CGPT-WEB", + terminalPollFailure + ? `conversation poll stopped before finished_successfully; returning latest assistant text for ${conversationId}` + : `conversation poll timed out before finished_successfully; returning latest assistant text for ${conversationId}` + ); + } else { + ctx.log?.warn?.( + "CGPT-WEB", + terminalPollFailure + ? `conversation poll stopped without assistant text for ${conversationId}` + : `conversation poll timed out without assistant text for ${conversationId}` + ); + } + return last; +} + // ─── OpenAI SSE format ────────────────────────────────────────────────────── function sseChunk(data: unknown): string { @@ -1474,6 +1755,11 @@ function buildStreamingResponse( // stream finishes without an image_asset_pointer. The executor passes a // closure here that knows how to poll the conversation endpoint. pollAsyncImage: ((conversationId: string) => Promise) | null, + // Optional poller for GPT-5.5 Pro's stream_handoff path. Inline text keeps + // streaming as-is; once ChatGPT hands off, we append the final assistant + // answer fetched from the conversation detail endpoint. Text requests stay + // in Temporary Chat, so these polls should not create sidebar/history items. + pollFinalAnswer: ((conversationId: string) => Promise) | null, log: { warn?: (tag: string, msg: string) => void } | null, signal?: AbortSignal | null ): ReadableStream { @@ -1501,11 +1787,79 @@ function buildStreamingResponse( let conversationId: string | null = null; let imagePointers: ImagePointerRef[] | undefined; let imageGenAsync = false; + let handoff = false; + let emittedText = ""; + let polledFinalAnswer = ""; let parentCandidateMessageId: string | null = null; + const emitTextDelta = (content: string): void => { + const cleaned = cleanChatGptText(content); + if (!cleaned) return; + emittedText += cleaned; + controller.enqueue( + encoder.encode( + sseChunk({ + id: cid, + object: "chat.completion.chunk", + created, + model, + system_fingerprint: null, + choices: [ + { + index: 0, + delta: { content: cleaned }, + finish_reason: null, + logprobs: null, + }, + ], + }) + ) + ); + }; + + const appendFinalAnswer = (text: string): void => { + const cleaned = cleanChatGptText(text); + const finalTrimmed = cleaned.trim(); + if (!finalTrimmed) return; + const emittedTrimmed = emittedText.trim(); + if (emittedTrimmed === finalTrimmed || emittedTrimmed.endsWith(finalTrimmed)) return; + const prefix = emittedTrimmed && !emittedText.endsWith("\n") ? "\n\n" : ""; + emitTextDelta(`${prefix}${cleaned}`); + }; + + // Heartbeat: long async work (Pro polling, WebSocket image-gen, + // 2-3 MB image fetch) leaves the SSE quiet and Open WebUI times out + // at ~30s (`disconnect: ResponseAborted`). SSE comments and empty + // `delta:{}` chunks are both filtered upstream + // (`hasValuableContent` in open-sse/utils/streamHelpers.ts), so + // heartbeats are zero-width-space content deltas (`"​"`): they pass + // the filter and render invisibly. + const startHeartbeat = (intervalMs = 5_000): (() => void) => { + const heartbeatChunk = sseChunk({ + id: cid, + object: "chat.completion.chunk", + created, + model, + system_fingerprint: null, + choices: [{ index: 0, delta: { content: "​" }, finish_reason: null, logprobs: null }], + }); + const timer = setInterval(() => { + try { + controller.enqueue(encoder.encode(heartbeatChunk)); + } catch { + // Controller may already be closed if the client disconnected + // — just stop firing. + console.warn("[chatgpt-web] heartbeat enqueue failed - controller closed"); + clearInterval(timer); + } + }, intervalMs); + return () => clearInterval(timer); + }; + for await (const chunk of extractContent(eventStream, signal)) { if (chunk.conversationId) conversationId = chunk.conversationId; if (chunk.messageId) parentCandidateMessageId = chunk.messageId; + if (chunk.handoff) handoff = true; if (chunk.error) { controller.enqueue( encoder.encode( @@ -1532,67 +1886,36 @@ function buildStreamingResponse( if (chunk.done) { imagePointers = chunk.imagePointers; imageGenAsync = chunk.imageGenAsync ?? false; + handoff = handoff || (chunk.handoff ?? false); if (chunk.messageId) parentCandidateMessageId = chunk.messageId; break; } if (chunk.delta) { - const cleaned = cleanChatGptText(chunk.delta); - if (cleaned) { - controller.enqueue( - encoder.encode( - sseChunk({ - id: cid, - object: "chat.completion.chunk", - created, - model, - system_fingerprint: null, - choices: [ - { - index: 0, - delta: { content: cleaned }, - finish_reason: null, - logprobs: null, - }, - ], - }) - ) - ); - } + emitTextDelta(chunk.delta); } } + if (pollFinalAnswer && conversationId && handoff) { + const stopHb = startHeartbeat(); + try { + const polled = await pollFinalAnswer(conversationId); + if (polled?.text) { + polledFinalAnswer = polled.text; + if (polled.messageId) parentCandidateMessageId = polled.messageId; + } + } finally { + stopHb(); + } + } + + if (polledFinalAnswer) { + appendFinalAnswer(polledFinalAnswer); + } + // Async image_gen ends the SSE with a "Processing image..." // placeholder; poll the conversation endpoint in the background for // the final pointer (only when in-stream pointers are empty). - // Heartbeat: long async work (WebSocket image-gen, 2-3 MB image - // fetch) leaves the SSE quiet and Open WebUI times out at ~30s - // (`disconnect: ResponseAborted`). SSE comments and empty `delta:{}` - // chunks are both filtered upstream (`hasValuableContent` in - // open-sse/utils/streamHelpers.ts), so heartbeats are zero-width-space - // content deltas (`"​"`): they pass the filter and render invisibly. - const startHeartbeat = (intervalMs = 5_000): (() => void) => { - const heartbeatChunk = sseChunk({ - id: cid, - object: "chat.completion.chunk", - created, - model, - system_fingerprint: null, - choices: [{ index: 0, delta: { content: "​" }, finish_reason: null, logprobs: null }], - }); - const timer = setInterval(() => { - try { - controller.enqueue(encoder.encode(heartbeatChunk)); - } catch { - // Controller may already be closed if the client disconnected - // — just stop firing. - console.warn("[chatgpt-web] heartbeat enqueue failed - controller closed"); - clearInterval(timer); - } - }, intervalMs); - return () => clearInterval(timer); - }; - if ( imageGenAsync && conversationId && @@ -1754,6 +2077,7 @@ async function buildNonStreamingResponse( currentMsg: string, resolver: ImageResolver | null, pollAsyncImage: ((conversationId: string) => Promise) | null, + pollFinalAnswer: ((conversationId: string) => Promise) | null, log: { warn?: (tag: string, msg: string) => void } | null, signal?: AbortSignal | null ): Promise { @@ -1761,11 +2085,13 @@ async function buildNonStreamingResponse( let conversationId: string | null = null; let imagePointers: ImagePointerRef[] | undefined; let imageGenAsync = false; + let handoff = false; let parentCandidateMessageId: string | null = null; for await (const chunk of extractContent(eventStream, signal)) { if (chunk.conversationId) conversationId = chunk.conversationId; if (chunk.messageId) parentCandidateMessageId = chunk.messageId; + if (chunk.handoff) handoff = true; if (chunk.error) { return new Response( JSON.stringify({ @@ -1778,12 +2104,21 @@ async function buildNonStreamingResponse( fullAnswer = chunk.answer || fullAnswer; imagePointers = chunk.imagePointers; imageGenAsync = chunk.imageGenAsync ?? false; + handoff = handoff || (chunk.handoff ?? false); if (chunk.messageId) parentCandidateMessageId = chunk.messageId; break; } if (chunk.answer) fullAnswer = chunk.answer; } + if (pollFinalAnswer && conversationId && (handoff || !fullAnswer.trim())) { + const polled = await pollFinalAnswer(conversationId); + if (polled?.text) { + fullAnswer = polled.text; + if (polled.messageId) parentCandidateMessageId = polled.messageId; + } + } + fullAnswer = cleanChatGptText(fullAnswer); // Async image gen: SSE ended with "Processing image..." — poll for the @@ -2557,18 +2892,16 @@ export class ChatGptWebExecutor extends BaseExecutor { log ); - // 2a''. Apply thinking_effort preference for thinking-capable models. - // Mirrors what chatgpt.com's web UI does when the user toggles the - // "Standard"/"Extended" thinking switch — PATCH the user-config endpoint - // before issuing the conversation. The conversation request itself has - // no `thinking_effort` field; the server reads the stored preference at - // routing time. Best-effort: a failed PATCH falls back to whatever the - // account's current preference is. - const earlyModelSlug = MODEL_MAP[model] ?? model; - const requestedEffort = resolveThinkingEffort(body, credentials.providerSpecificData); - if (requestedEffort && isThinkingCapableModel(model, earlyModelSlug)) { + // 2a''. Resolve model + effort and apply thinking-effort preference for + // thinking-capable models. Dedicated thinking models mirror the browser's + // user-config PATCH; GPT-5.5 Pro sends the effort with the conversation + // body because the Pro standard/extended budget is part of that turn. + const resolvedModel = resolveChatGptModel(model, body, credentials.providerSpecificData); + const modelSlug = resolvedModel.slug; + const requestedEffort = resolvedModel.effort; + if (requestedEffort && isThinkingCapableModel(model, modelSlug)) { await setUserThinkingEffort( - earlyModelSlug, + modelSlug, requestedEffort, tokenEntry.accessToken, tokenEntry.accountId, @@ -2660,13 +2993,13 @@ export class ChatGptWebExecutor extends BaseExecutor { }; } - // Toggle Temporary Chat off only for image-generation requests, since - // Temporary Chat disables the image_gen tool. For plain text turns we - // keep Temporary Chat on so the user's chatgpt.com history isn't - // polluted with router traffic. + // Toggle Temporary Chat off only when ChatGPT needs a durable image + // conversation. Text requests, including GPT-5.5 Pro, stay temporary so + // they do not show up in the user's chatgpt.com sidebar/history. const imageEdit = looksLikeImageEditRequest(parsed); const continuation = imageEdit ? parsed.latestImageContext : null; const forImageGen = looksLikeImageGenRequest(parsed) || imageEdit; + const persistConversation = forImageGen || !!continuation; if (forImageGen) { log?.debug?.( "CGPT-WEB", @@ -2674,17 +3007,16 @@ export class ChatGptWebExecutor extends BaseExecutor { ? "Image edit intent detected — continuing saved image conversation" : "Image-gen intent detected — disabling Temporary Chat for this turn" ); + } else if (resolvedModel.isPro) { + log?.debug?.("CGPT-WEB", "GPT-5.5 Pro text request — keeping Temporary Chat enabled"); } const parentMessageId = continuation?.parentMessageId ?? randomUUID(); - const modelSlug = MODEL_MAP[model] ?? model; - const cgptBody = buildConversationBody( - parsed, - modelSlug, - parentMessageId, - forImageGen, - continuation - ); + const cgptBody = buildConversationBody(parsed, modelSlug, parentMessageId, { + persistConversation, + thinkingEffort: requestedEffort, + continuation, + }); const headers: Record = { ...browserHeaders(), @@ -2785,6 +3117,9 @@ export class ChatGptWebExecutor extends BaseExecutor { const imageResolver = makeImageResolver(resolverCtx); const pollAsyncImage = (conversationId: string) => pollForAsyncImage(conversationId, resolverCtx); + const pollFinalAnswer = resolvedModel.isPro + ? (conversationId: string) => pollForFinalAssistantAnswer(conversationId, resolverCtx) + : null; // Tool mode buffers (no live streaming) and is gated off the image-gen path. const toolMode = hasTools && !forImageGen; @@ -2798,6 +3133,7 @@ export class ChatGptWebExecutor extends BaseExecutor { created, imageResolver, pollAsyncImage, + pollFinalAnswer, log, signal ); @@ -2818,6 +3154,7 @@ export class ChatGptWebExecutor extends BaseExecutor { parsed.currentMsg, imageResolver, pollAsyncImage, + pollFinalAnswer, log, signal ); diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index 7da5c49b9b..65d09889e3 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -40,7 +40,6 @@ import { T3ChatWebExecutor } from "./t3-chat-web.ts"; import { ClaudeWebExecutor } from "./claude-web.ts"; import { InnerAiExecutor } from "./inner-ai.ts"; import { HuggingChatExecutor } from "./huggingchat.ts"; -import { PhindExecutor } from "./phind.ts"; import { PoeWebExecutor } from "./poe-web.ts"; import { VeniceWebExecutor } from "./venice-web.ts"; import { V0VercelWebExecutor } from "./v0-vercel-web.ts"; @@ -128,8 +127,6 @@ const executors = { "in-ai": new InnerAiExecutor(), // Alias huggingchat: new HuggingChatExecutor(), hc: new HuggingChatExecutor(), // Alias - phind: new PhindExecutor(), - ph: new PhindExecutor(), // Alias "poe-web": new PoeWebExecutor(), poe: new PoeWebExecutor(), // Alias "venice-web": new VeniceWebExecutor(), diff --git a/open-sse/executors/mimocode.ts b/open-sse/executors/mimocode.ts index a3730488c2..45a061bae3 100644 --- a/open-sse/executors/mimocode.ts +++ b/open-sse/executors/mimocode.ts @@ -18,7 +18,8 @@ import * as crypto from "node:crypto"; import * as os from "node:os"; import { BaseExecutor, type ExecuteInput, type ProviderCredentials } from "./base.ts"; -import { runWithProxyContext } from "../utils/proxyFetch.ts"; +import { createProxyDispatcher } from "../utils/proxyDispatcher.ts"; +import { fetch as undiciFetch, type Dispatcher } from "undici"; const BOOTSTRAP_PATH = "/api/free-ai/bootstrap"; const CHAT_PATH = "/api/free-ai/openai/chat"; @@ -91,8 +92,6 @@ interface AccountState { expiresAt: number; cooldownUntil: number; consecutiveFails: number; - /** Resolved proxy config for this account (null = direct). */ - proxy: AccountProxyConfig["proxy"]; } function parseJwtExp(jwt: string): number { @@ -149,7 +148,8 @@ const bootstrapInflight = new Map { const existing = bootstrapInflight.get(fingerprint); if (existing) return existing; @@ -162,12 +162,20 @@ async function bootstrapJwt( const promise = (async () => { try { - const resp = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ client: fingerprint }), - signal: controller.signal, - }); + const resp = dispatcher + ? await undiciFetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ client: fingerprint }), + signal: controller.signal, + dispatcher, + }) + : await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ client: fingerprint }), + signal: controller.signal, + }); if (!resp.ok) { const body = await resp.text().catch(() => ""); throw new Error(`Bootstrap failed: ${resp.status} ${body.slice(0, 200)}`); @@ -199,6 +207,7 @@ export class MimocodeExecutor extends BaseExecutor { private accounts: AccountState[] = []; private nextAccountIdx = 0; private baseUrl: string; + private proxyUrlMap = new Map(); private static encoder = new TextEncoder(); constructor() { @@ -210,12 +219,67 @@ export class MimocodeExecutor extends BaseExecutor { expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, + // #3837/#5521 backward compat: default the per-account proxy to null (not undefined), + // mirroring the syncAccountsFromCredentials() account builder, so an executor with no + // accountProxies config still exposes `acct.proxy === null` on every account. proxy: null, }); } + private getProxyDispatcher(fingerprint: string): Dispatcher | undefined { + const proxyUrl = this.proxyUrlMap.get(fingerprint); + if (!proxyUrl) return undefined; + return createProxyDispatcher(proxyUrl); + } + + private fetchWithProxy(url: string, init: RequestInit, fingerprint: string): Promise { + const dispatcher = this.getProxyDispatcher(fingerprint); + if (dispatcher) { + // undici fetch returns undici.Response which is structurally compatible with + // the global Response but nominally different — same pattern as proxyFetch.ts + const undiciFn = undiciFetch as unknown as ( + url: string, + init: RequestInit & { dispatcher?: unknown } + ) => Promise; + return undiciFn(url, { ...init, dispatcher }); + } + return fetch(url, init); + } + private syncAccountsFromCredentials(credentials: ProviderCredentials): void { - const fingerprints = credentials?.providerSpecificData?.fingerprints; + const psd = credentials?.providerSpecificData; + const fingerprints = psd?.fingerprints; + + const accountProxies = psd?.accountProxies as AccountProxyConfig[] | undefined; + + // #5521: build the per-fingerprint proxy URL map that getProxyDispatcher() consumes + // to route each account's traffic through its own SOCKS5/HTTP dispatcher. + if (Array.isArray(accountProxies)) { + for (const entry of accountProxies) { + if (entry?.fingerprint && entry?.proxy?.host) { + const { + type = "socks5", + host, + port, + username, + password, + } = entry.proxy as { + type?: string; + host: string; + port?: number; + username?: string; + password?: string; + }; + const resolvedPort = port ?? (type === "socks5" ? 1080 : 8080); + const auth = username + ? `${encodeURIComponent(username)}:${password ? encodeURIComponent(password) : ""}@` + : ""; + this.proxyUrlMap.set(entry.fingerprint, `${type}://${auth}${host}:${resolvedPort}`); + } + } + } + + // #3837: register any newly-advertised fingerprints as accounts. if (Array.isArray(fingerprints)) { const existing = new Set(this.accounts.map((a) => a.fingerprint)); for (const fp of fingerprints) { @@ -233,13 +297,10 @@ export class MimocodeExecutor extends BaseExecutor { } } - const accountProxies = credentials?.providerSpecificData?.accountProxies as - | AccountProxyConfig[] - | undefined; + // #3837: resolve each account's structured proxy config from accountProxies. const proxyMap = Array.isArray(accountProxies) ? new Map(accountProxies.map((ap) => [ap.fingerprint, ap.proxy] as const)) : null; - for (const acct of this.accounts) { if (proxyMap) { const entry = proxyMap.get(acct.fingerprint); @@ -255,10 +316,8 @@ export class MimocodeExecutor extends BaseExecutor { signal?: AbortSignal | null ): Promise { if (isAccountReady(account)) return account.jwt; - const proxy = account.proxy; - const result = await runWithProxyContext(proxy, () => - bootstrapJwt(this.baseUrl, account.fingerprint, signal) - ); + const dispatcher = this.getProxyDispatcher(account.fingerprint); + const result = await bootstrapJwt(this.baseUrl, account.fingerprint, signal, dispatcher); account.jwt = result.jwt; account.expiresAt = result.expiresAt; return account.jwt; @@ -337,9 +396,9 @@ export class MimocodeExecutor extends BaseExecutor { this.syncAccountsFromCredentials(_credentials); const account = this.accounts[0]; const jwt = await this.getJwtForAccount(account, _signal); - const proxy = account.proxy; - const resp = await runWithProxyContext(proxy, () => - fetch(this.buildUrl("mimo-auto", false), { + const resp = await this.fetchWithProxy( + this.buildUrl("mimo-auto", false), + { method: "POST", headers: { "Content-Type": "application/json", @@ -354,7 +413,8 @@ export class MimocodeExecutor extends BaseExecutor { }) ), signal: _signal ?? undefined, - }) + }, + account.fingerprint ); return resp.status === 200; } catch { @@ -400,15 +460,16 @@ export class MimocodeExecutor extends BaseExecutor { const jwt = await this.getJwtForAccount(account, signal); const headers = this.buildHeaders(input.credentials, stream); headers["Authorization"] = `Bearer ${jwt}`; - const proxy = account.proxy; - let resp = await runWithProxyContext(proxy, () => - fetch(url, { + let resp = await this.fetchWithProxy( + url, + { method: "POST", headers, body: JSON.stringify(reqBody), signal: signal ?? undefined, - }) + }, + account.fingerprint ); // On auth failure, re-bootstrap this account and retry once @@ -422,13 +483,15 @@ export class MimocodeExecutor extends BaseExecutor { account.consecutiveFails = 0; const freshJwt = await this.getJwtForAccount(account, signal); headers["Authorization"] = `Bearer ${freshJwt}`; - resp = await runWithProxyContext(proxy, () => - fetch(url, { + resp = await this.fetchWithProxy( + url, + { method: "POST", headers, body: JSON.stringify(reqBody), signal: signal ?? undefined, - }) + }, + account.fingerprint ); } diff --git a/open-sse/executors/muse-spark-web.ts b/open-sse/executors/muse-spark-web.ts index f7f9ce9b1a..6a7cac1f1d 100644 --- a/open-sse/executors/muse-spark-web.ts +++ b/open-sse/executors/muse-spark-web.ts @@ -629,7 +629,7 @@ function classifyMetaAiError(errorMessage: string | null, content: string) { if (/authentication required to send messages|login is required|sign in/i.test(combined)) { return { status: 401, - message: "Meta AI auth failed — your meta.ai abra_sess cookie may be missing or expired.", + message: "Meta AI auth failed — your meta.ai ecto_1_sess cookie may be missing or expired.", }; } diff --git a/open-sse/executors/phind.ts b/open-sse/executors/phind.ts deleted file mode 100644 index 778178414a..0000000000 --- a/open-sse/executors/phind.ts +++ /dev/null @@ -1,250 +0,0 @@ -/** - * PhindExecutor — Free Dev-Focused AI Chat via phind.com - * - * Routes requests through Phind's chat API. - * Free tier available. Uses session cookie for auth. - * - * Endpoint: POST https://www.phind.com/api/agent - * Auth: Session cookie from phind.com - * SSE response with data: prefixed JSON chunks (OpenAI-compatible delta format) - */ -import { BaseExecutor, type ExecuteInput } from "./base.ts"; -import { makeExecutorErrorResult as makeErrorResult, normalizeCookie } from "../utils/error.ts"; - -const BASE_URL = "https://www.phind.com"; -const CHAT_URL = `${BASE_URL}/api/agent`; -const USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"; - -export class PhindExecutor extends BaseExecutor { - constructor() { - super("phind", { id: "phind", baseUrl: "https://www.phind.com" }); - } - - async execute(input: ExecuteInput) { - const { body, credentials, signal, stream: wantStream } = input; - const bodyObj = (body || {}) as Record; - const rawCookie = String(credentials?.apiKey ?? "").trim(); - const cookie = normalizeCookie(rawCookie); - - // Build Phind-format messages - const messages = (bodyObj.messages as Array<{ role: string; content: string }>) || []; - const phindMessages = messages.map((m) => ({ role: m.role, content: m.content })); - const modelId = (bodyObj.model as string) || "phind-model"; - // Last user message as userInput (Phind expects this field) - const lastUserMsg = [...messages].reverse().find((m) => m.role === "user"); - const userInput = lastUserMsg?.content || ""; - - const reqBody = { - userInput, - messages: phindMessages, - requestedModel: modelId, - webSearchMode: "auto", - isChromeExtension: false, - language: "en-US", - date: new Date().toISOString(), - }; - - const reqHeaders: Record = { - "Content-Type": "application/json;charset=UTF-8", - "User-Agent": USER_AGENT, - Accept: "text/event-stream", - Referer: `${BASE_URL}/`, - Origin: BASE_URL, - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "same-origin", - }; - if (cookie) reqHeaders.Cookie = cookie; - - let upstream: Response; - try { - upstream = await fetch(CHAT_URL, { - method: "POST", - headers: reqHeaders, - body: JSON.stringify(reqBody), - signal, - }); - } catch (err) { - return makeErrorResult( - 502, - `Phind fetch failed: ${err instanceof Error ? err.message : "unknown"}`, - body, - CHAT_URL - ); - } - - if (!upstream.ok) { - const errText = await upstream.text().catch(() => ""); - return makeErrorResult(upstream.status, `Phind error: ${errText}`, body, CHAT_URL); - } - - // Phind always returns SSE — parse it for both streaming and non-streaming - if (!upstream.body) { - return makeErrorResult(502, "Phind returned empty response body", body, CHAT_URL); - } - - if (!wantStream) { - // Collect all SSE chunks into a single response - const reader = upstream.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - let fullText = ""; - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - for (const line of lines) { - if (!line.startsWith("data:")) continue; - const data = line.slice(5).trim(); - if (data === "[DONE]") continue; - try { - const parsed = JSON.parse(data); - const text = parsed.choices?.[0]?.delta?.content || parsed.content || ""; - if (text) fullText += text; - } catch { - // Skip unparseable chunks - } - } - } - } catch (err) { - if (!signal?.aborted) { - return makeErrorResult( - 502, - `Phind stream read failed: ${err instanceof Error ? err.message : "unknown"}`, - body, - CHAT_URL - ); - } - } - - return { - response: new Response( - JSON.stringify({ - id: `chatcmpl-ph-${Date.now()}`, - object: "chat.completion", - created: Math.floor(Date.now() / 1000), - model: modelId, - choices: [ - { - index: 0, - message: { role: "assistant", content: fullText }, - finish_reason: "stop", - }, - ], - usage: { - prompt_tokens: Math.ceil((userInput || "").length / 4), - completion_tokens: Math.ceil(fullText.length / 4), - total_tokens: Math.ceil(((userInput || "").length + fullText.length) / 4), - }, - }), - { headers: { "Content-Type": "application/json" } } - ), - url: CHAT_URL, - headers: reqHeaders, - transformedBody: reqBody, - }; - } - - // Streaming: transform Phind SSE to OpenAI format - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - const sseStream = new ReadableStream({ - async start(controller) { - const reader = upstream.body!.getReader(); - let buffer = ""; - try { - // Initial role chunk - controller.enqueue( - encoder.encode( - `data: ${JSON.stringify({ - id: `chatcmpl-ph-${Date.now()}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: modelId, - choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }], - })}\n\n` - ) - ); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - - for (const line of lines) { - if (!line.startsWith("data:")) continue; - const data = line.slice(5).trim(); - if (data === "[DONE]") { - controller.enqueue(encoder.encode("data: [DONE]\n\n")); - continue; - } - try { - const parsed = JSON.parse(data); - const text = parsed.choices?.[0]?.delta?.content || parsed.content || ""; - if (text) { - const chunk = { - id: `chatcmpl-ph-${Date.now()}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: modelId, - choices: [{ index: 0, delta: { content: text }, finish_reason: null }], - }; - controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)); - } - } catch { - // Skip unparseable chunks - } - } - } - } catch (err) { - if (!signal?.aborted) { - controller.enqueue( - encoder.encode( - `data: ${JSON.stringify({ - id: `chatcmpl-ph-${Date.now()}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: modelId, - choices: [ - { - index: 0, - delta: { - content: `[Stream error: ${err instanceof Error ? err.message : String(err)}]`, - }, - finish_reason: "stop", - }, - ], - })}\n\n` - ) - ); - } - } finally { - controller.enqueue(encoder.encode("data: [DONE]\n\n")); - controller.close(); - } - }, - }); - - return { - response: new Response(sseStream, { - status: 200, - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - "X-Accel-Buffering": "no", - }, - }), - url: CHAT_URL, - headers: reqHeaders, - transformedBody: reqBody, - }; - } -} diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index fb371aa1cb..8f9a810cf3 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -9,6 +9,7 @@ import { applyClientUsageBuffer } from "./chatCore/clientUsageBuffer.ts"; import { buildPostCallGuardrailContext } from "./chatCore/postCallGuardrailContext.ts"; import { storeSemanticCacheResponse } from "./chatCore/semanticCacheStore.ts"; import { buildNonStreamingResponseHeaders } from "./chatCore/nonStreamingResponseHeaders.ts"; +import { buildNonStreamingJsonResponse } from "./chatCore/nonStreamingJsonResponse.ts"; import { maybeConvertJsonBodyToSse } from "./chatCore/jsonBodyToSse.ts"; import { assembleStreamingResponseHeaders } from "./chatCore/streamingResponseHeaders.ts"; import { storeStreamingSemanticCacheResponse } from "./chatCore/streamingSemanticCacheStore.ts"; @@ -3897,9 +3898,7 @@ export async function handleChatCore({ if (echoModel) echoModelInObject(translatedResponse, echoModel); return { success: true, - response: new Response(JSON.stringify(translatedResponse), { - headers: responseHeaders, - }), + response: buildNonStreamingJsonResponse(translatedResponse, responseHeaders), }; } diff --git a/open-sse/handlers/chatCore/nonStreamingJsonResponse.ts b/open-sse/handlers/chatCore/nonStreamingJsonResponse.ts new file mode 100644 index 0000000000..3b3d91848e --- /dev/null +++ b/open-sse/handlers/chatCore/nonStreamingJsonResponse.ts @@ -0,0 +1,16 @@ +/** + * Build the final non-streaming JSON response body once and publish an accurate + * Content-Length for downstream HTTP clients and buffering proxies. + */ +export function buildNonStreamingJsonResponse( + body: unknown, + headers: Record +): Response { + const payload = JSON.stringify(body); + return new Response(payload, { + headers: { + ...headers, + "Content-Length": String(Buffer.byteLength(payload)), + }, + }); +} diff --git a/open-sse/package.json b/open-sse/package.json index e414d4071a..c301e08bc7 100644 --- a/open-sse/package.json +++ b/open-sse/package.json @@ -1,6 +1,6 @@ { "name": "@omniroute/open-sse", - "version": "3.8.41", + "version": "3.8.42", "description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration", "type": "module", "main": "index.js", diff --git a/open-sse/services/__tests__/tierResolver.test.ts b/open-sse/services/__tests__/tierResolver.test.ts index 07a91a19fe..458a567627 100644 --- a/open-sse/services/__tests__/tierResolver.test.ts +++ b/open-sse/services/__tests__/tierResolver.test.ts @@ -44,7 +44,7 @@ describe("TierResolver", () => { }); it("classifies LongCat as free", () => { - const result = classifyTier("longcat", "flash-lite"); + const result = classifyTier("longcat", "LongCat-2.0"); assert.equal(result.tier, PROVIDER_TIER.FREE); assert.equal(result.hasFreeTier, true); }); diff --git a/open-sse/services/chatgptTlsClient.ts b/open-sse/services/chatgptTlsClient.ts index 5b3276684f..8d4d85a180 100644 --- a/open-sse/services/chatgptTlsClient.ts +++ b/open-sse/services/chatgptTlsClient.ts @@ -14,7 +14,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; -import { mkdtemp, open, unlink, rmdir, stat } from "node:fs/promises"; +import { mkdtemp, open, unlink, rmdir, stat, readFile } from "node:fs/promises"; import { randomUUID } from "node:crypto"; let clientPromise: Promise | null = null; @@ -31,6 +31,8 @@ const DEFAULT_TIMEOUT_MS = // longer than the configured timeout when the binding is dead. const HARD_TIMEOUT_GRACE_MS = Number.parseInt(process.env.OMNIROUTE_CHATGPT_TLS_GRACE_MS || "", 10) || 10_000; +const STREAM_FIRST_BYTE_TIMEOUT_MS = + Number.parseInt(process.env.OMNIROUTE_CHATGPT_STREAM_FIRST_BYTE_TIMEOUT_MS || "", 10) || 30_000; function installExitHook(): void { if (exitHookInstalled) return; @@ -293,7 +295,8 @@ export async function tlsFetchChatGpt( requestOptions, options.streamEofSymbol, options.signal ?? null, - (options.timeoutMs ?? DEFAULT_TIMEOUT_MS) + HARD_TIMEOUT_GRACE_MS + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS) + HARD_TIMEOUT_GRACE_MS, + STREAM_FIRST_BYTE_TIMEOUT_MS ); } @@ -350,7 +353,8 @@ async function tlsFetchStreaming( requestOptions: Record, eofSymbol = "[DONE]", signal: AbortSignal | null = null, - hardTimeoutMs: number = DEFAULT_TIMEOUT_MS + HARD_TIMEOUT_GRACE_MS + hardTimeoutMs: number = DEFAULT_TIMEOUT_MS + HARD_TIMEOUT_GRACE_MS, + firstByteTimeoutMs: number = STREAM_FIRST_BYTE_TIMEOUT_MS ): Promise { const dir = await mkdtemp(join(tmpdir(), "cgpt-stream-")); const path = join(dir, `${randomUUID()}.sse`); @@ -392,16 +396,21 @@ async function tlsFetchStreaming( // that race; if the request actually fails before producing any bytes, // the timeout falls through to the requestPromise drain below (returning // the real upstream status). - const ready = await waitForContent(path, 5_000, requestPromise); + const ready = await waitForContent(path, firstByteTimeoutMs, requestPromise); if (!ready) { const r = await requestPromise.catch( (e) => ({ status: 502, headers: {}, body: String(e) }) as TlsResponseLike ); + // If the first byte arrived after our first-byte wait but before the + // request settled, tls-client-node may have written the full SSE body to + // streamOutputPath while leaving r.body empty. Prefer those captured bytes + // over misclassifying a successful delayed stream as "empty response body". + const fileText = await readTextFileIfExists(path); await cleanupTempPath(path); return { status: r.status, headers: toHeaders(r.headers), - text: r.body, + text: fileText || r.body, body: null, }; } @@ -417,11 +426,12 @@ async function tlsFetchStreaming( const r = await requestPromise.catch( (e) => ({ status: 502, headers: {}, body: String(e) }) as TlsResponseLike ); + const fileText = await readTextFileIfExists(path); await cleanupTempPath(path); return { status: r.status, headers: toHeaders(r.headers), - text: r.body, + text: r.body || fileText, body: null, }; } @@ -457,6 +467,34 @@ async function cleanupTempPath(path: string): Promise { await rmdir(dir).catch(() => {}); } +async function readTextFileIfExists(path: string): Promise { + try { + return await readFile(path, "utf8"); + } catch { + return ""; + } +} + +export async function __tlsFetchStreamingForTesting( + client: { request: (url: string, opts: Record) => Promise }, + url: string, + requestOptions: Record, + eofSymbol = "[DONE]", + signal: AbortSignal | null = null, + hardTimeoutMs: number = DEFAULT_TIMEOUT_MS + HARD_TIMEOUT_GRACE_MS, + firstByteTimeoutMs: number = STREAM_FIRST_BYTE_TIMEOUT_MS +): Promise { + return tlsFetchStreaming( + client as { request: (url: string, opts: Record) => Promise }, + url, + requestOptions, + eofSymbol, + signal, + hardTimeoutMs, + firstByteTimeoutMs + ); +} + async function readFirstBytes(path: string, n: number): Promise { const fd = await open(path, "r"); try { diff --git a/open-sse/services/combo/quotaStrategies.ts b/open-sse/services/combo/quotaStrategies.ts index 1f5ca8d7b7..bb9dd42037 100644 --- a/open-sse/services/combo/quotaStrategies.ts +++ b/open-sse/services/combo/quotaStrategies.ts @@ -150,6 +150,141 @@ function getTargetConnectionIds( return connectionIds; } +async function expandTargetsByQuotaAwareConnections( + targets: ResolvedComboTarget[], + comboName: string, + log: { warn?: (...args: unknown[]) => void }, + apiKeyAllowedConnectionIds?: string[] | null +): Promise<{ + connectionById: Map>; + expandedTargets: ResolvedComboTarget[]; +}> { + const connectionCache = new Map>>(); + const connectionLoadPromises = new Map>>>(); + const connectionById = new Map>(); + const expandedTargets: ResolvedComboTarget[] = []; + + const targetsWithConnections = await Promise.all( + targets.map(async (target) => ({ + connections: await getQuotaAwareConnectionsForTarget( + target, + connectionCache, + connectionLoadPromises, + comboName, + log + ), + target, + })) + ); + + for (const { target, connections } of targetsWithConnections) { + for (const connection of connections) { + if (typeof connection.id === "string") connectionById.set(connection.id, connection); + } + + const unrestrictedConnectionIds = getTargetConnectionIds(target, connections); + const connectionIds = filterAllowedConnectionIds( + unrestrictedConnectionIds, + apiKeyAllowedConnectionIds + ); + if (connectionIds.length === 0) { + if ( + unrestrictedConnectionIds.length > 0 && + normalizeConnectionIds(apiKeyAllowedConnectionIds) + ) { + continue; + } + expandedTargets.push(target); + continue; + } + + for (const connectionId of connectionIds) { + expandedTargets.push({ + ...target, + connectionId, + executionKey: + target.connectionId === connectionId + ? target.executionKey + : `${target.executionKey}@${connectionId}`, + }); + } + } + + return { connectionById, expandedTargets }; +} + +async function scoreQuotaAwareTargets({ + comboName, + config, + connectionById, + expandedTargets, + log, + scoreQuota, +}: { + comboName: string; + config: QuotaFetchCacheConfig; + connectionById: Map>; + expandedTargets: ResolvedComboTarget[]; + log: { warn?: (...args: unknown[]) => void }; + scoreQuota: (quota: unknown) => TScore; +}): Promise> { + const quotaPromises = new Map>(); + + return mapWithConcurrency( + expandedTargets, + RESET_AWARE_QUOTA_FETCH_CONCURRENCY, + async (target, index) => { + let quota: unknown = null; + const provider = getResetAwareProvider(target); + const fetcher = provider ? getQuotaFetcher(provider) : null; + if (fetcher && provider && target.connectionId) { + const quotaKey = `${provider}:${target.connectionId}`; + if (!quotaPromises.has(quotaKey)) { + quotaPromises.set( + quotaKey, + fetchResetAwareQuotaWithCache({ + provider, + connectionId: target.connectionId, + connection: connectionById.get(target.connectionId), + fetcher, + config, + log, + comboName, + }) + ); + } + quota = await quotaPromises.get(quotaKey)!; + } + + return { target, index, ...scoreQuota(quota) }; + } + ); +} + +function rotateLeadingTies( + sortedTargets: T[], + tiedTargets: T[], + key: string +): T[] { + let orderedTiedTargets = tiedTargets; + if (tiedTargets.length > 1) { + const counter = rrCounters.get(key) || 0; + if (!rrCounters.has(key) && rrCounters.size >= MAX_RR_COUNTERS) { + const oldest = rrCounters.keys().next().value; + if (oldest !== undefined) rrCounters.delete(oldest); + } + rrCounters.set(key, counter + 1); + const startIndex = counter % tiedTargets.length; + orderedTiedTargets = [...tiedTargets.slice(startIndex), ...tiedTargets.slice(0, startIndex)]; + } + + const tiedExecutionKeys = new Set(orderedTiedTargets.map((entry) => entry.target.executionKey)); + return [ + ...orderedTiedTargets, + ...sortedTargets.filter((entry) => !tiedExecutionKeys.has(entry.target.executionKey)), + ]; +} + async function mapWithConcurrency( items: T[], concurrency: number, @@ -329,87 +464,21 @@ export async function orderTargetsByResetAwareQuota( if (targets.length === 0) return targets; const config = resolveResetAwareConfig(configSource); - const connectionCache = new Map>>(); - const connectionLoadPromises = new Map>>>(); - const quotaPromises = new Map>(); - const connectionById = new Map>(); - const expandedTargets: ResolvedComboTarget[] = []; - - const targetsWithConnections = await Promise.all( - targets.map(async (target) => ({ - connections: await getQuotaAwareConnectionsForTarget( - target, - connectionCache, - connectionLoadPromises, - comboName, - log - ), - target, - })) + const { connectionById, expandedTargets } = await expandTargetsByQuotaAwareConnections( + targets, + comboName, + log, + apiKeyAllowedConnectionIds ); - for (const { target, connections } of targetsWithConnections) { - for (const connection of connections) { - if (typeof connection.id === "string") connectionById.set(connection.id, connection); - } - - const unrestrictedConnectionIds = getTargetConnectionIds(target, connections); - const connectionIds = filterAllowedConnectionIds( - unrestrictedConnectionIds, - apiKeyAllowedConnectionIds - ); - if (connectionIds.length === 0) { - if ( - unrestrictedConnectionIds.length > 0 && - normalizeConnectionIds(apiKeyAllowedConnectionIds) - ) { - continue; - } - expandedTargets.push(target); - continue; - } - - for (const connectionId of connectionIds) { - expandedTargets.push({ - ...target, - connectionId, - executionKey: - target.connectionId === connectionId - ? target.executionKey - : `${target.executionKey}@${connectionId}`, - }); - } - } - - const scoredTargets = await mapWithConcurrency( + const scoredTargets = await scoreQuotaAwareTargets({ + comboName, + config, + connectionById, expandedTargets, - RESET_AWARE_QUOTA_FETCH_CONCURRENCY, - async (target, index) => { - let quota: unknown = null; - const provider = getResetAwareProvider(target); - const fetcher = provider ? getQuotaFetcher(provider) : null; - if (fetcher && provider && target.connectionId) { - const quotaKey = `${provider}:${target.connectionId}`; - if (!quotaPromises.has(quotaKey)) { - quotaPromises.set( - quotaKey, - fetchResetAwareQuotaWithCache({ - provider, - connectionId: target.connectionId, - connection: connectionById.get(target.connectionId), - fetcher, - config, - log, - comboName, - }) - ); - } - quota = await quotaPromises.get(quotaKey)!; - } - const { score } = scoreResetAwareQuota(quota, config); - return { target, score, index }; - } - ); + log, + scoreQuota: (quota) => ({ score: scoreResetAwareQuota(quota, config).score }), + }); scoredTargets.sort((a, b) => { if (b.score !== a.score) return b.score - a.score; @@ -418,24 +487,9 @@ export async function orderTargetsByResetAwareQuota( const bestScore = scoredTargets[0]?.score ?? 0; const tiedTargets = scoredTargets.filter((entry) => bestScore - entry.score <= config.tieBand); - let orderedTiedTargets = tiedTargets; - if (tiedTargets.length > 1) { - const key = `reset-aware:${comboName}`; - const counter = rrCounters.get(key) || 0; - if (!rrCounters.has(key) && rrCounters.size >= MAX_RR_COUNTERS) { - const oldest = rrCounters.keys().next().value; - if (oldest !== undefined) rrCounters.delete(oldest); - } - rrCounters.set(key, counter + 1); - const startIndex = counter % tiedTargets.length; - orderedTiedTargets = [...tiedTargets.slice(startIndex), ...tiedTargets.slice(0, startIndex)]; - } - - const tiedExecutionKeys = new Set(orderedTiedTargets.map((entry) => entry.target.executionKey)); - return [ - ...orderedTiedTargets, - ...scoredTargets.filter((entry) => !tiedExecutionKeys.has(entry.target.executionKey)), - ].map((entry) => entry.target); + return rotateLeadingTies(scoredTargets, tiedTargets, `reset-aware:${comboName}`).map( + (entry) => entry.target + ); } export async function orderTargetsByResetWindow( @@ -448,91 +502,21 @@ export async function orderTargetsByResetWindow( if (targets.length === 0) return targets; const config = resolveResetWindowConfig(configSource); - const connectionCache = new Map>>(); - const connectionLoadPromises = new Map>>>(); - const quotaPromises = new Map>(); - const connectionById = new Map>(); - const expandedTargets: ResolvedComboTarget[] = []; - - const targetsWithConnections = await Promise.all( - targets.map(async (target) => ({ - connections: await getQuotaAwareConnectionsForTarget( - target, - connectionCache, - connectionLoadPromises, - comboName, - log - ), - target, - })) + const { connectionById, expandedTargets } = await expandTargetsByQuotaAwareConnections( + targets, + comboName, + log, + apiKeyAllowedConnectionIds ); - for (const { target, connections } of targetsWithConnections) { - for (const connection of connections) { - if (typeof connection.id === "string") connectionById.set(connection.id, connection); - } - - const unrestrictedConnectionIds = getTargetConnectionIds(target, connections); - const connectionIds = filterAllowedConnectionIds( - unrestrictedConnectionIds, - apiKeyAllowedConnectionIds - ); - if (connectionIds.length === 0) { - if ( - unrestrictedConnectionIds.length > 0 && - normalizeConnectionIds(apiKeyAllowedConnectionIds) - ) { - continue; - } - expandedTargets.push(target); - continue; - } - - for (const connectionId of connectionIds) { - expandedTargets.push({ - ...target, - connectionId, - executionKey: - target.connectionId === connectionId - ? target.executionKey - : `${target.executionKey}@${connectionId}`, - }); - } - } - - const scoredTargets = await mapWithConcurrency( + const scoredTargets = await scoreQuotaAwareTargets({ + comboName, + config, + connectionById, expandedTargets, - RESET_AWARE_QUOTA_FETCH_CONCURRENCY, - async (target, index) => { - let quota: unknown = null; - const provider = getResetAwareProvider(target); - const fetcher = provider ? getQuotaFetcher(provider) : null; - if (fetcher && provider && target.connectionId) { - const quotaKey = `${provider}:${target.connectionId}`; - if (!quotaPromises.has(quotaKey)) { - quotaPromises.set( - quotaKey, - fetchResetAwareQuotaWithCache({ - provider, - connectionId: target.connectionId, - connection: connectionById.get(target.connectionId), - fetcher, - config, - log, - comboName, - }) - ); - } - quota = await quotaPromises.get(quotaKey)!; - } - - return { - target, - resetMs: getResetWindowTimestampMs(quota, config.windows), - index, - }; - } - ); + log, + scoreQuota: (quota) => ({ resetMs: getResetWindowTimestampMs(quota, config.windows) }), + }); scoredTargets.sort((a, b) => { if (a.resetMs !== b.resetMs) return a.resetMs - b.resetMs; @@ -549,24 +533,9 @@ export async function orderTargetsByResetWindow( ); if (tiedTargets.length <= 1) return scoredTargets.map((entry) => entry.target); - const key = `reset-window:${comboName}`; - const counter = rrCounters.get(key) || 0; - if (!rrCounters.has(key) && rrCounters.size >= MAX_RR_COUNTERS) { - const oldest = rrCounters.keys().next().value; - if (oldest !== undefined) rrCounters.delete(oldest); - } - rrCounters.set(key, counter + 1); - const startIndex = counter % tiedTargets.length; - const orderedTiedTargets = [ - ...tiedTargets.slice(startIndex), - ...tiedTargets.slice(0, startIndex), - ]; - const tiedExecutionKeys = new Set(orderedTiedTargets.map((entry) => entry.target.executionKey)); - - return [ - ...orderedTiedTargets, - ...scoredTargets.filter((entry) => !tiedExecutionKeys.has(entry.target.executionKey)), - ].map((entry) => entry.target); + return rotateLeadingTies(scoredTargets, tiedTargets, `reset-window:${comboName}`).map( + (entry) => entry.target + ); } /** @@ -616,54 +585,13 @@ export async function orderTargetsByHeadroom( if (targets.length <= 1) return targets; try { - const connectionCache = new Map>>(); - const connectionLoadPromises = new Map>>>(); - const expandedTargets: ResolvedComboTarget[] = []; - - const targetsWithConnections = await Promise.all( - targets.map(async (target) => ({ - connections: await getQuotaAwareConnectionsForTarget( - target, - connectionCache, - connectionLoadPromises, - comboName, - log - ), - target, - })) + const { expandedTargets } = await expandTargetsByQuotaAwareConnections( + targets, + comboName, + log, + apiKeyAllowedConnectionIds ); - for (const { target, connections } of targetsWithConnections) { - const unrestrictedConnectionIds = getTargetConnectionIds(target, connections); - const connectionIds = filterAllowedConnectionIds( - unrestrictedConnectionIds, - apiKeyAllowedConnectionIds - ); - if (connectionIds.length === 0) { - if ( - unrestrictedConnectionIds.length > 0 && - normalizeConnectionIds(apiKeyAllowedConnectionIds) - ) { - continue; - } - // No specific connection — keep the target with full headroom (it cannot - // be saturation-scored, so it should not be penalized). - expandedTargets.push(target); - continue; - } - - for (const connectionId of connectionIds) { - expandedTargets.push({ - ...target, - connectionId, - executionKey: - target.connectionId === connectionId - ? target.executionKey - : `${target.executionKey}@${connectionId}`, - }); - } - } - if (expandedTargets.length <= 1) return expandedTargets; const getSaturation = await resolveHeadroomSaturationFetcher(); @@ -672,28 +600,32 @@ export async function orderTargetsByHeadroom( const satByConnection = new Map>(); const connKey = (target: ResolvedComboTarget) => `${target.provider}:${target.connectionId}`; - await mapWithConcurrency(expandedTargets, HEADROOM_SATURATION_FETCH_CONCURRENCY, async (target) => { - if (!target.connectionId) return; - const key = connKey(target); - if (satByConnection.has(key)) return; - satByConnection.set( - key, - (async () => { - const [util5h, util7d] = await Promise.all([ - getSaturation(target.connectionId as string, target.provider, { - unit: "percent", - window: "5h", - }), - getSaturation(target.connectionId as string, target.provider, { - unit: "percent", - window: "weekly", - }), - ]); - return { util5h, util7d } satisfies HeadroomSaturation; - })() - ); - await satByConnection.get(key); - }); + await mapWithConcurrency( + expandedTargets, + HEADROOM_SATURATION_FETCH_CONCURRENCY, + async (target) => { + if (!target.connectionId) return; + const key = connKey(target); + if (satByConnection.has(key)) return; + satByConnection.set( + key, + (async () => { + const [util5h, util7d] = await Promise.all([ + getSaturation(target.connectionId as string, target.provider, { + unit: "percent", + window: "5h", + }), + getSaturation(target.connectionId as string, target.provider, { + unit: "percent", + window: "weekly", + }), + ]); + return { util5h, util7d } satisfies HeadroomSaturation; + })() + ); + await satByConnection.get(key); + } + ); // Resolve the per-connection saturation, keyed by the per-target executionKey // for the pure ranker. Targets without a connection get full headroom. @@ -706,7 +638,10 @@ export async function orderTargetsByHeadroom( return rankByHeadroom(expandedTargets, satByExecutionKey, (target) => target.executionKey); } catch (err) { - log.warn?.({ err: (err as Error)?.message, comboName }, "headroom ordering failed — keeping target order"); + log.warn?.( + { err: (err as Error)?.message, comboName }, + "headroom ordering failed — keeping target order" + ); return targets; } } diff --git a/open-sse/services/compression/engines/rtk/commandDetector.ts b/open-sse/services/compression/engines/rtk/commandDetector.ts index 6f58a87b6a..8b932bb583 100644 --- a/open-sse/services/compression/engines/rtk/commandDetector.ts +++ b/open-sse/services/compression/engines/rtk/commandDetector.ts @@ -27,6 +27,9 @@ type Detector = { const COMMAND_PREFIXES = [ "git", "make", + "gradle", + "gradlew", + "dotnet", "terraform", "tofu", "opentofu", @@ -114,6 +117,18 @@ const DETECTORS: Detector[] = [ commandPatterns: [/^make\b/i], contentPatterns: [/^make\[\d+\]: (?:Entering|Leaving) directory/m, /make: \*\*\* /], }, + { + type: "gradle", + category: "build", + commandPatterns: [/^(?:gradle|gradlew|\.\/gradlew)\b/i], + contentPatterns: [/^> Task :/m, /^BUILD (?:SUCCESSFUL|FAILED)\b/m], + }, + { + type: "dotnet", + category: "build", + commandPatterns: [/^dotnet\s+(?:build|test|run|restore|publish|pack|msbuild)\b/i, /^dotnet\b/i], + contentPatterns: [/^Build (?:succeeded|FAILED)\b/m, /\b(?:error|warning) CS\d+\b/m], + }, { type: "terraform-plan", category: "infra", diff --git a/open-sse/services/compression/engines/rtk/filters/dotnet.json b/open-sse/services/compression/engines/rtk/filters/dotnet.json new file mode 100644 index 0000000000..532f4575c8 --- /dev/null +++ b/open-sse/services/compression/engines/rtk/filters/dotnet.json @@ -0,0 +1,47 @@ +{ + "id": "dotnet", + "label": ".NET CLI", + "description": "Strip the .NET build banner and restore chatter; keep build/test status, warnings (CS####), and errors.", + "category": "build", + "priority": 78, + "match": { + "outputTypes": ["dotnet"], + "commands": ["^dotnet\\b"], + "patterns": ["^Build (?:succeeded|FAILED)\\b", "\\b(?:error|warning) CS\\d+\\b"] + }, + "rules": { + "stripAnsi": true, + "dropPatterns": [ + "^Microsoft \\(R\\)", + "^Copyright \\(C\\) Microsoft", + "Determining projects to restore", + "^\\s*Restored ", + "^\\s*$", + "^Time Elapsed " + ], + "collapsePatterns": ["^\\s*Restored "], + "deduplicate": true, + "maxLines": 160, + "headLines": 40, + "tailLines": 60, + "onEmpty": "dotnet: ok" + }, + "preserve": { + "errorPatterns": [ + "Build FAILED", + "FAILED", + "\\berror CS\\d+\\b", + "\\bwarning CS\\d+\\b", + "Failed!" + ], + "summaryPatterns": ["Build succeeded", "Passed!", "Total tests:"] + }, + "tests": [ + { + "name": "drops build banner + restore chatter, keeps status", + "command": "dotnet build", + "input": "Microsoft (R) Build Engine version 17.8\n Determining projects to restore...\nBuild succeeded.\n", + "expected": "Build succeeded." + } + ] +} diff --git a/open-sse/services/compression/engines/rtk/filters/gradle.json b/open-sse/services/compression/engines/rtk/filters/gradle.json new file mode 100644 index 0000000000..e01096bb4d --- /dev/null +++ b/open-sse/services/compression/engines/rtk/filters/gradle.json @@ -0,0 +1,47 @@ +{ + "id": "gradle", + "label": "Gradle", + "description": "Strip Gradle daemon/welcome chatter and no-op tasks; keep build status, failures, and warnings.", + "category": "build", + "priority": 78, + "match": { + "outputTypes": ["gradle"], + "commands": ["^(?:gradle|gradlew|\\./gradlew)\\b"], + "patterns": ["^> Task :", "^BUILD (?:SUCCESSFUL|FAILED)\\b"] + }, + "rules": { + "stripAnsi": true, + "dropPatterns": [ + "^Welcome to Gradle", + "^Starting a Gradle Daemon", + "^> Task :\\S+ (?:UP-TO-DATE|SKIPPED|NO-SOURCE|FROM-CACHE)$", + "^Download https?://", + "^\\s*$" + ], + "collapsePatterns": ["^> Task :\\S+$"], + "deduplicate": true, + "maxLines": 160, + "headLines": 40, + "tailLines": 60, + "onEmpty": "gradle: ok" + }, + "preserve": { + "errorPatterns": [ + "BUILD FAILED", + "FAILED", + "What went wrong", + "Caused by:", + "error:", + "> Task :\\S+ FAILED" + ], + "summaryPatterns": ["BUILD SUCCESSFUL", "actionable task"] + }, + "tests": [ + { + "name": "drops welcome + up-to-date tasks, keeps build status", + "command": "gradle build", + "input": "Welcome to Gradle 8.5!\n> Task :app:compileJava UP-TO-DATE\nBUILD SUCCESSFUL in 3s\n", + "expected": "BUILD SUCCESSFUL in 3s" + } + ] +} diff --git a/open-sse/services/compression/languageDetector.ts b/open-sse/services/compression/languageDetector.ts index 230d6086f8..bbd4a79f83 100644 --- a/open-sse/services/compression/languageDetector.ts +++ b/open-sse/services/compression/languageDetector.ts @@ -17,6 +17,14 @@ const LANGUAGE_HINTS: Record = { * zero hits → English. (B-LANG-DETECTOR) */ export function detectCompressionLanguage(text: string): string { + // CJK disambiguation: Han ideographs (U+4E00–U+9FFF) are shared by Chinese and Japanese, but + // kana (U+3040–U+30FF) is Japanese-exclusive. Text with Han and no kana is Chinese (zh); text + // with kana falls through to the scorer below, where the `ja` kana hint catches it. Keeping zh + // out of the additive scorer means a Han-heavy Japanese sentence is never misread as Chinese. + if (/[一-鿿]/.test(text) && !/[぀-ヿ]/.test(text)) { + return "zh"; + } + let best = "en"; let bestScore = 0; for (const [language, patterns] of Object.entries(LANGUAGE_HINTS)) { @@ -37,5 +45,7 @@ export function detectCompressionLanguage(text: string): string { } export function listSupportedCompressionLanguages(): string[] { - return ["en", ...Object.keys(LANGUAGE_HINTS)]; + // zh is detected via the CJK Han/kana disambiguation in detectCompressionLanguage rather than a + // keyword hint (so it stays out of the additive scorer), hence it is listed explicitly here. + return ["en", "zh", ...Object.keys(LANGUAGE_HINTS)]; } diff --git a/open-sse/services/compression/pipelineGuards.ts b/open-sse/services/compression/pipelineGuards.ts new file mode 100644 index 0000000000..07a712e306 --- /dev/null +++ b/open-sse/services/compression/pipelineGuards.ts @@ -0,0 +1,45 @@ +/** + * Aggregate guards for the stacked compression pipeline (T02 / Headroom H1). + * + * These operate on the WHOLE pipeline result, distinct from the opt-in per-step TV1 bail-out + * (`decideStep` in `strategySelector.ts`): TV1 governs whether to ADVANCE between steps and is + * default-off; the inflation guard here is an honest DEFAULT-ON check on the FINAL output. + */ + +export interface PipelineInflationInput { + /** The verbatim request body before any engine ran. */ + originalBody: Record; + /** The fully-stacked body after the pipeline ran. */ + compressedBody: Record; + /** Token count of `originalBody` (already measured by the stats pass). */ + originalTokens: number; + /** Token count of `compressedBody` (already measured by the stats pass). */ + compressedTokens: number; +} + +export interface PipelineInflationResult { + /** The body to actually use: the original when the pipeline did not shrink it. */ + body: Record; + /** True when the stacked output did not shrink the input, so the original was kept. */ + inflated: boolean; +} + +/** + * Honest aggregate inflation guard. If the fully-stacked body did not actually shrink — its token + * count is `>=` the original — the compressed body is discarded and the verbatim original is + * returned. + * + * Safe by construction: the only alternative it ever returns is `originalBody`, the unmodified + * request, which is always a valid payload. A (rare) false trigger therefore can never corrupt a + * payload — it only forgoes a compression that saved nothing. + * + * `originalTokens === 0` (empty/degenerate input) is treated as "not inflated" so an empty body is + * never spuriously flagged. + */ +export function guardPipelineInflation(input: PipelineInflationInput): PipelineInflationResult { + const { originalTokens, compressedTokens } = input; + if (originalTokens > 0 && compressedTokens >= originalTokens) { + return { body: input.originalBody, inflated: true }; + } + return { body: input.compressedBody, inflated: false }; +} diff --git a/open-sse/services/compression/rules/de/dedup.json b/open-sse/services/compression/rules/de/dedup.json new file mode 100644 index 0000000000..057c376e73 --- /dev/null +++ b/open-sse/services/compression/rules/de/dedup.json @@ -0,0 +1,38 @@ +{ + "language": "de", + "category": "dedup", + "rules": [ + { + "name": "de_repeated_context", + "pattern": "\\b(?:wie bereits besprochen|wie zuvor erwähnt|wie bereits erwähnt|wie ich bereits sagte)\\b[,.]?\\s*", + "replacement": "Siehe oben. ", + "context": "all", + "category": "dedup", + "minIntensity": "lite" + }, + { + "name": "de_repeated_question", + "pattern": "\\b(?:dieselbe Frage wie zuvor|das habe ich bereits gefragt|das ist dieselbe Frage)\\b[,.]?\\s*", + "replacement": "[gleiche Frage] ", + "context": "user", + "category": "dedup", + "minIntensity": "lite" + }, + { + "name": "de_reestablished_context", + "pattern": "\\b(?:zurück zum obigen Code|bezugnehmend auf|zurückkommend auf)\\b\\s*", + "replacement": "Re: ", + "context": "all", + "category": "dedup", + "minIntensity": "lite" + }, + { + "name": "de_summary_replacement", + "pattern": "\\b(?:um zusammenzufassen was wir besprochen haben|zusammenfassend unser Gespräch|um es zusammenzufassen)\\b[,.]?\\s*", + "replacement": "Zusammenfassung: ", + "context": "assistant", + "category": "dedup", + "minIntensity": "lite" + } + ] +} diff --git a/open-sse/services/compression/rules/de/ultra.json b/open-sse/services/compression/rules/de/ultra.json new file mode 100644 index 0000000000..4b5a140bc9 --- /dev/null +++ b/open-sse/services/compression/rules/de/ultra.json @@ -0,0 +1,107 @@ +{ + "language": "de", + "category": "ultra", + "rules": [ + { + "name": "de_articles", + "pattern": "\\b(?:[Dd]er|[Dd]ie|[Dd]as|[Dd]en|[Dd]em|[Dd]es|[Ee]in|[Ee]ine|[Ee]inen|[Ee]inem|[Ee]iner|[Ee]ines)\\s+(?=[a-zäöüß])", + "flags": "g", + "replacement": "", + "context": "all", + "category": "terse", + "minIntensity": "full" + }, + { + "name": "de_leader_phrases", + "pattern": "^(?:ich werde|ich kann|ich würde|lass mich|wir werden|wir können|lass uns)\\s+(?=[a-zäöüß])", + "replacement": "", + "context": "all", + "category": "terse", + "minIntensity": "full" + }, + { + "name": "de_ultra_database_abbreviation", + "pattern": "\\bDatenbank\\b", + "replacement": "DB", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "de_ultra_config_abbreviation", + "pattern": "\\bKonfiguration\\b", + "replacement": "Konfig", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "de_ultra_function_abbreviation", + "pattern": "\\bFunktion\\b", + "replacement": "Fn", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "de_ultra_request_abbreviation", + "pattern": "\\b(?:Anfrage|Anforderung)\\b", + "replacement": "Req", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "de_ultra_response_abbreviation", + "pattern": "\\bAntwort\\b", + "replacement": "Res", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "de_ultra_implementation_abbreviation", + "pattern": "\\bImplementierung\\b", + "replacement": "Impl", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "de_ultra_authentication_abbreviation", + "pattern": "\\bAuthentifizierung\\b", + "replacement": "Auth", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "de_ultra_authorization_abbreviation", + "pattern": "\\bAutorisierung\\b", + "replacement": "Authz", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "de_ultra_application_abbreviation", + "pattern": "\\b(?:Anwendung|Applikation)\\b", + "replacement": "App", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "de_ultra_dependency_abbreviation", + "pattern": "\\b(?:Abhängigkeit|Abhängigkeiten)\\b", + "replacement": "Dep", + "replacementMap": { + "Abhängigkeit": "Dep", + "Abhängigkeiten": "Deps" + }, + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + } + ] +} diff --git a/open-sse/services/compression/rules/fr/dedup.json b/open-sse/services/compression/rules/fr/dedup.json new file mode 100644 index 0000000000..75222cfa01 --- /dev/null +++ b/open-sse/services/compression/rules/fr/dedup.json @@ -0,0 +1,38 @@ +{ + "language": "fr", + "category": "dedup", + "rules": [ + { + "name": "fr_repeated_context", + "pattern": "\\b(?:comme discuté précédemment|comme mentionné précédemment|comme indiqué précédemment|comme je l'ai dit avant)\\b[,.]?\\s*", + "replacement": "Voir ci-dessus. ", + "context": "all", + "category": "dedup", + "minIntensity": "lite" + }, + { + "name": "fr_repeated_question", + "pattern": "\\b(?:même question qu'avant|j'ai déjà posé cette question|c'est la même question)\\b[,.]?\\s*", + "replacement": "[même question] ", + "context": "user", + "category": "dedup", + "minIntensity": "lite" + }, + { + "name": "fr_reestablished_context", + "pattern": "\\b(?:revenons au code ci-dessus|en référence à|revenant à)\\b\\s*", + "replacement": "Re: ", + "context": "all", + "category": "dedup", + "minIntensity": "lite" + }, + { + "name": "fr_summary_replacement", + "pattern": "\\b(?:pour résumer ce que nous avons discuté|en résumé de notre conversation|pour récapituler)\\b[,.]?\\s*", + "replacement": "Résumé: ", + "context": "assistant", + "category": "dedup", + "minIntensity": "lite" + } + ] +} diff --git a/open-sse/services/compression/rules/fr/ultra.json b/open-sse/services/compression/rules/fr/ultra.json new file mode 100644 index 0000000000..3c1f88562c --- /dev/null +++ b/open-sse/services/compression/rules/fr/ultra.json @@ -0,0 +1,107 @@ +{ + "language": "fr", + "category": "ultra", + "rules": [ + { + "name": "fr_articles", + "pattern": "\\b(?:[Ll]e|[Ll]a|[Ll]es|[Uu]n|[Uu]ne|[Dd]es|[Dd]u)\\s+(?=[a-zàâçéèêëîïôûùüÿœ])", + "flags": "g", + "replacement": "", + "context": "all", + "category": "terse", + "minIntensity": "full" + }, + { + "name": "fr_leader_phrases", + "pattern": "^(?:je vais|je peux|nous allons|nous pouvons|laisse-moi|on peut)\\s+(?=[a-zàâçéèêëîïôûùüÿœ])", + "replacement": "", + "context": "all", + "category": "terse", + "minIntensity": "full" + }, + { + "name": "fr_ultra_database_abbreviation", + "pattern": "\\bbase de données\\b", + "replacement": "BD", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "fr_ultra_config_abbreviation", + "pattern": "\\bconfiguration\\b", + "replacement": "config", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "fr_ultra_function_abbreviation", + "pattern": "\\bfonction\\b", + "replacement": "fn", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "fr_ultra_request_abbreviation", + "pattern": "\\b(?:requête|demande)\\b", + "replacement": "req", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "fr_ultra_response_abbreviation", + "pattern": "\\bréponse\\b", + "replacement": "rép", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "fr_ultra_implementation_abbreviation", + "pattern": "\\bimplémentation\\b", + "replacement": "impl", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "fr_ultra_authentication_abbreviation", + "pattern": "\\bauthentification\\b", + "replacement": "auth", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "fr_ultra_authorization_abbreviation", + "pattern": "\\bautorisation\\b", + "replacement": "authz", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "fr_ultra_application_abbreviation", + "pattern": "\\bapplication\\b", + "replacement": "app", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "fr_ultra_dependency_abbreviation", + "pattern": "\\b(?:dépendance|dépendances)\\b", + "replacement": "dép", + "replacementMap": { + "dépendance": "dép", + "dépendances": "déps" + }, + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + } + ] +} diff --git a/open-sse/services/compression/rules/ja/dedup.json b/open-sse/services/compression/rules/ja/dedup.json new file mode 100644 index 0000000000..e9674c2e44 --- /dev/null +++ b/open-sse/services/compression/rules/ja/dedup.json @@ -0,0 +1,38 @@ +{ + "language": "ja", + "category": "dedup", + "rules": [ + { + "name": "ja_repeated_context", + "pattern": "(?:前述のとおり|前に述べたように|以前述べたように|先ほど言ったように)", + "replacement": "(上記参照)", + "context": "all", + "category": "dedup", + "minIntensity": "lite" + }, + { + "name": "ja_repeated_question", + "pattern": "(?:前と同じ質問ですが|これは前と同じ質問です|先ほどと同じ質問ですが)", + "replacement": "[同じ質問]", + "context": "user", + "category": "dedup", + "minIntensity": "lite" + }, + { + "name": "ja_reestablished_context", + "pattern": "(?:上記のコードに戻ると|先ほどの話に戻ると)", + "replacement": "Re: ", + "context": "all", + "category": "dedup", + "minIntensity": "lite" + }, + { + "name": "ja_summary_replacement", + "pattern": "(?:これまでの議論を要約すると|会話を要約すると|まとめると)", + "replacement": "要約:", + "context": "assistant", + "category": "dedup", + "minIntensity": "lite" + } + ] +} diff --git a/open-sse/services/compression/rules/ja/ultra.json b/open-sse/services/compression/rules/ja/ultra.json new file mode 100644 index 0000000000..1deea66448 --- /dev/null +++ b/open-sse/services/compression/rules/ja/ultra.json @@ -0,0 +1,86 @@ +{ + "language": "ja", + "category": "ultra", + "rules": [ + { + "name": "ja_ultra_database_abbreviation", + "pattern": "データベース", + "replacement": "DB", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "ja_ultra_config_abbreviation", + "pattern": "(?:コンフィギュレーション|設定)", + "replacement": "config", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "ja_ultra_function_abbreviation", + "pattern": "関数", + "replacement": "fn", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "ja_ultra_request_abbreviation", + "pattern": "リクエスト", + "replacement": "req", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "ja_ultra_response_abbreviation", + "pattern": "レスポンス", + "replacement": "res", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "ja_ultra_implementation_abbreviation", + "pattern": "実装", + "replacement": "impl", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "ja_ultra_authentication_abbreviation", + "pattern": "認証", + "replacement": "auth", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "ja_ultra_authorization_abbreviation", + "pattern": "認可", + "replacement": "authz", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "ja_ultra_application_abbreviation", + "pattern": "アプリケーション", + "replacement": "app", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "ja_ultra_dependency_abbreviation", + "pattern": "依存関係", + "replacement": "dep", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + } + ] +} diff --git a/open-sse/services/compression/rules/zh/dedup.json b/open-sse/services/compression/rules/zh/dedup.json new file mode 100644 index 0000000000..1f5669adea --- /dev/null +++ b/open-sse/services/compression/rules/zh/dedup.json @@ -0,0 +1,38 @@ +{ + "language": "zh", + "category": "dedup", + "rules": [ + { + "name": "zh_repeated_context", + "pattern": "(?:如前所述|如之前所说|如先前提到|正如我之前所说)", + "replacement": "见上。", + "context": "all", + "category": "dedup", + "minIntensity": "lite" + }, + { + "name": "zh_repeated_question", + "pattern": "(?:和之前一样的问题|我之前问过这个问题|这是同一个问题)", + "replacement": "[同问]", + "context": "user", + "category": "dedup", + "minIntensity": "lite" + }, + { + "name": "zh_reestablished_context", + "pattern": "(?:回到上面的代码|关于之前提到的内容|回到之前的话题)", + "replacement": "Re: ", + "context": "all", + "category": "dedup", + "minIntensity": "lite" + }, + { + "name": "zh_summary_replacement", + "pattern": "(?:总结一下我们讨论的内容|总结我们的对话|概括来说)", + "replacement": "总结:", + "context": "assistant", + "category": "dedup", + "minIntensity": "lite" + } + ] +} diff --git a/open-sse/services/compression/rules/zh/filler.json b/open-sse/services/compression/rules/zh/filler.json new file mode 100644 index 0000000000..33b789f7f8 --- /dev/null +++ b/open-sse/services/compression/rules/zh/filler.json @@ -0,0 +1,54 @@ +{ + "language": "zh", + "category": "filler", + "rules": [ + { + "name": "zh_filler_please", + "pattern": "请(?:你|您)?(?:帮我|帮忙)?", + "replacement": "", + "context": "user", + "category": "filler", + "minIntensity": "lite" + }, + { + "name": "zh_filler_thanks", + "pattern": "(?:谢谢|多谢|感谢)(?:你|您|大家)?[。,!]?", + "replacement": "", + "context": "all", + "category": "filler", + "minIntensity": "lite" + }, + { + "name": "zh_filler_trouble", + "pattern": "(?:麻烦你|麻烦您|劳驾)[,]?", + "replacement": "", + "context": "user", + "category": "filler", + "minIntensity": "lite" + }, + { + "name": "zh_filler_greeting", + "pattern": "(?:你好|您好|大家好)[,,]?", + "replacement": "", + "context": "all", + "category": "filler", + "minIntensity": "lite" + }, + { + "name": "zh_filler_hedge_think", + "pattern": "(?:我觉得|我认为|我想说)", + "replacement": "", + "context": "all", + "category": "filler", + "minIntensity": "full" + }, + { + "name": "zh_filler_hedge_actually", + "pattern": "(?:其实|说实话|基本上)[,,]?", + "replacement": "", + "context": "all", + "category": "filler", + "minIntensity": "full" + } + ] +} diff --git a/open-sse/services/compression/rules/zh/ultra.json b/open-sse/services/compression/rules/zh/ultra.json new file mode 100644 index 0000000000..0c15a6fca1 --- /dev/null +++ b/open-sse/services/compression/rules/zh/ultra.json @@ -0,0 +1,55 @@ +{ + "language": "zh", + "category": "ultra", + "rules": [ + { + "name": "zh_ultra_modal_particles", + "pattern": "(?:吗|呢|吧|啊|呀|嘛)(?=[。,!?、\\s]|$)", + "flags": "g", + "replacement": "", + "context": "all", + "category": "terse", + "minIntensity": "full" + }, + { + "name": "zh_ultra_database_abbreviation", + "pattern": "数据库", + "replacement": "DB", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "zh_ultra_application_abbreviation", + "pattern": "应用程序", + "replacement": "app", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "zh_ultra_dependency_abbreviation", + "pattern": "依赖关系", + "replacement": "dep", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "zh_ultra_config_file_abbreviation", + "pattern": "配置文件", + "replacement": "cfg", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + }, + { + "name": "zh_ultra_function_abbreviation", + "pattern": "函数", + "replacement": "fn", + "context": "all", + "category": "ultra", + "minIntensity": "ultra" + } + ] +} diff --git a/open-sse/services/compression/strategySelector.ts b/open-sse/services/compression/strategySelector.ts index df41ad2a8e..baa180599d 100644 --- a/open-sse/services/compression/strategySelector.ts +++ b/open-sse/services/compression/strategySelector.ts @@ -14,6 +14,7 @@ import { cavemanCompress } from "./caveman.ts"; import { compressAggressive } from "./aggressive.ts"; import { ultraCompress, ultraCompressHeuristic } from "./ultra.ts"; import { createCompressionStats } from "./stats.ts"; +import { guardPipelineInflation } from "./pipelineGuards.ts"; import { registerBuiltinCompressionEngines } from "./engines/index.ts"; import { getCompressionEngine, getEngineEntry } from "./engines/registry.ts"; import { applyRtkCompression } from "./engines/rtk/index.ts"; @@ -787,6 +788,30 @@ function finalizeStackedResult( return true; }); } + + // T02 / H1: honest aggregate inflation guard. If the fully-stacked body did not actually shrink + // (its token count is >= the original), discard it and return the verbatim original — safe by + // construction, since the original request body is always a valid payload. + const inflation = guardPipelineInflation({ + originalBody, + compressedBody: currentBody, + originalTokens: stats.originalTokens, + compressedTokens: stats.compressedTokens, + }); + if (inflation.inflated) { + const inflatedTokens = stats.compressedTokens; + const warnings = new Set(stats.validationWarnings ?? []); + warnings.add( + `pipeline-inflation-guard: stacked output (${inflatedTokens} tok) did not shrink input ` + + `(${stats.originalTokens} tok); reverted to original` + ); + stats.validationWarnings = Array.from(warnings); + stats.fallbackApplied = true; + stats.compressedTokens = stats.originalTokens; + stats.savingsPercent = 0; + return { body: inflation.body, compressed: false, stats }; + } + return { body: currentBody, compressed, stats }; } diff --git a/open-sse/services/providerCostData.ts b/open-sse/services/providerCostData.ts index 461caf0046..b1be9d0203 100644 --- a/open-sse/services/providerCostData.ts +++ b/open-sse/services/providerCostData.ts @@ -26,11 +26,11 @@ export const KNOWN_MODEL_PRICING: Record = { "grok-4-fast": { inputCostPer1M: 0.2, outputCostPer1M: 0.5, isFree: false }, "kimi-k2-thinking": { inputCostPer1M: 0, outputCostPer1M: 0, isFree: true }, "qwen3-coder-plus": { inputCostPer1M: 0, outputCostPer1M: 0, isFree: true }, - "longcat-flash-lite": { - inputCostPer1M: 0, - outputCostPer1M: 0, + "longcat-2.0": { + inputCostPer1M: 0.75, + outputCostPer1M: 2.95, isFree: true, - freeQuotaLimit: 50000000, + freeQuotaLimit: 10000000, }, }; diff --git a/open-sse/services/rateLimitManager.ts b/open-sse/services/rateLimitManager.ts index ea26bc9983..6adfe42c0b 100644 --- a/open-sse/services/rateLimitManager.ts +++ b/open-sse/services/rateLimitManager.ts @@ -140,9 +140,7 @@ function resolveMinTime(override: number | undefined | null): number { // Resolve a maxConcurrent override. 0 or missing means "effectively infinite". function resolveMaxConcurrent(override: number | undefined | null): number { - return typeof override === "number" && override > 0 - ? override - : EFFECTIVELY_INFINITE_CONCURRENCY; + return typeof override === "number" && override > 0 ? override : EFFECTIVELY_INFINITE_CONCURRENCY; } function buildLimiterDefaults() { @@ -159,14 +157,9 @@ function buildLimiterDefaults() { } function updateAllLimiterSettings() { + const defaults = buildLimiterDefaults(); for (const limiter of limiters.values()) { - limiter.updateSettings({ - maxConcurrent: currentRequestQueueSettings.concurrentRequests, - minTime: currentRequestQueueSettings.minTimeBetweenRequestsMs, - reservoir: currentRequestQueueSettings.requestsPerMinute, - reservoirRefreshAmount: currentRequestQueueSettings.requestsPerMinute, - reservoirRefreshInterval: 60 * 1000, - }); + limiter.updateSettings(defaults); } } @@ -234,7 +227,9 @@ function watchdogTick() { limiters.delete(key); lastDispatchAt.delete(key); limiterLastUsed.delete(key); - logRateLimit(`🧹 [RATE-LIMIT] Evicting idle limiter: ${key} (inactive for ${Math.round((now - lastUsed) / 1000)}s)`); + logRateLimit( + `🧹 [RATE-LIMIT] Evicting idle limiter: ${key} (inactive for ${Math.round((now - lastUsed) / 1000)}s)` + ); trackAsyncOperation(limiter.disconnect()); } } @@ -535,7 +530,12 @@ export async function withRateLimit(provider, connectionId, model, fn, signal = // Proactive sliding-window fallback for header-less providers with a declared cap // (Fase 8.2). No-op unless PROVIDER_DEFAULT_RATE_LIMITS has an entry for `provider`. - await awaitProviderDefaultSlot(provider, connectionId, signal, currentRequestQueueSettings.maxWaitMs); + await awaitProviderDefaultSlot( + provider, + connectionId, + signal, + currentRequestQueueSettings.maxWaitMs + ); const limiter = getLimiter(provider, connectionId, model); const maxWaitMs = currentRequestQueueSettings.maxWaitMs; diff --git a/open-sse/utils/sha3-512.ts b/open-sse/utils/sha3-512.ts new file mode 100644 index 0000000000..5c0f4cb106 --- /dev/null +++ b/open-sse/utils/sha3-512.ts @@ -0,0 +1,164 @@ +// Runtime-portable SHA3-512 (FIPS-202). +// +// The ChatGPT-Web sentinel proof-of-work (open-sse/executors/chatgpt-web.ts) +// hashes with SHA3-512. Node built against OpenSSL has it natively, but the +// **Electron desktop app** ships Node built against **BoringSSL, which does not +// implement the SHA-3 family** (electron/electron#30530). There, +// `createHash("sha3-512")` throws `Error: Digest method not supported`, so every +// chatgpt-web request fails with `502 ChatGPT sentinel failed: Digest method not +// supported` and the provider is unusable on the desktop app (#5531). +// +// This module prefers the native digest (fast path on servers / OpenSSL) and +// transparently falls back to a dependency-free pure-JS Keccak-f[1600] when the +// runtime's crypto lacks SHA-3. The pure-JS path is validated bit-for-bit +// against the native digest and the published FIPS-202 vectors in the unit test. + +import { createHash } from "node:crypto"; + +// ─── Keccak-f[1600] (BigInt lanes — correctness-first; only the fallback runs it) ── + +const MASK = (1n << 64n) - 1n; + +// Round constants RC[0..23]. +const RC: bigint[] = [ + 0x0000000000000001n, + 0x0000000000008082n, + 0x800000000000808an, + 0x8000000080008000n, + 0x000000000000808bn, + 0x0000000080000001n, + 0x8000000080008081n, + 0x8000000000008009n, + 0x000000000000008an, + 0x0000000000000088n, + 0x0000000080008009n, + 0x000000008000000an, + 0x000000008000808bn, + 0x800000000000008bn, + 0x8000000000008089n, + 0x8000000000008003n, + 0x8000000000008002n, + 0x8000000000000080n, + 0x000000000000800an, + 0x800000008000000an, + 0x8000000080008081n, + 0x8000000000008080n, + 0x0000000080000001n, + 0x8000000080008008n, +]; + +// Rotation offsets r[x+5y] (rho step). +const ROT: number[] = [ + 0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14, +]; + +function rotl64(x: bigint, n: number): bigint { + if (n === 0) return x; + const bn = BigInt(n); + return ((x << bn) | (x >> (64n - bn))) & MASK; +} + +function keccakF1600(s: bigint[]): void { + const C = new Array(5); + const D = new Array(5); + const B = new Array(25); + for (let round = 0; round < 24; round++) { + // θ + for (let x = 0; x < 5; x++) C[x] = s[x] ^ s[x + 5] ^ s[x + 10] ^ s[x + 15] ^ s[x + 20]; + for (let x = 0; x < 5; x++) D[x] = C[(x + 4) % 5] ^ rotl64(C[(x + 1) % 5], 1); + for (let x = 0; x < 5; x++) for (let y = 0; y < 5; y++) s[x + 5 * y] ^= D[x]; + // ρ + π + for (let x = 0; x < 5; x++) { + for (let y = 0; y < 5; y++) { + B[y + 5 * ((2 * x + 3 * y) % 5)] = rotl64(s[x + 5 * y], ROT[x + 5 * y]); + } + } + // χ + for (let x = 0; x < 5; x++) { + for (let y = 0; y < 5; y++) { + s[x + 5 * y] = B[x + 5 * y] ^ (~B[((x + 1) % 5) + 5 * y] & MASK & B[((x + 2) % 5) + 5 * y]); + } + } + // ι + s[0] ^= RC[round]; + } +} + +// SHA3-512: rate r = 576 bits (72 bytes / 9 lanes), capacity 1024, output 64 bytes. +const RATE_BYTES = 72; + +function sha3_512Bytes(msg: Uint8Array): Uint8Array { + const s: bigint[] = new Array(25).fill(0n); + + // FIPS-202 pad10*1 with SHA-3 domain separation (first pad byte 0x06, last |= 0x80). + const padLen = RATE_BYTES - (msg.length % RATE_BYTES); + const padded = new Uint8Array(msg.length + padLen); + padded.set(msg); + padded[msg.length] = 0x06; + padded[padded.length - 1] |= 0x80; + + // Absorb (little-endian lanes). + for (let off = 0; off < padded.length; off += RATE_BYTES) { + for (let i = 0; i < RATE_BYTES / 8; i++) { + let lane = 0n; + for (let b = 0; b < 8; b++) lane |= BigInt(padded[off + i * 8 + b]) << BigInt(8 * b); + s[i] ^= lane; + } + keccakF1600(s); + } + + // Squeeze 64 bytes (8 lanes — fits in one rate block). + const out = new Uint8Array(64); + for (let i = 0; i < 8; i++) { + const lane = s[i]; + for (let b = 0; b < 8; b++) out[i * 8 + b] = Number((lane >> BigInt(8 * b)) & 0xffn); + } + return out; +} + +function toBytes(input: string | Uint8Array): Uint8Array { + return typeof input === "string" ? new Uint8Array(Buffer.from(input, "utf8")) : input; +} + +/** Pure-JS SHA3-512 hex digest. Always runs the vendored Keccak (used as the fallback). */ +export function sha3_512HexJs(input: string | Uint8Array): string { + return Buffer.from(sha3_512Bytes(toBytes(input))).toString("hex"); +} + +type NativeHasher = (data: Uint8Array) => string; + +// undefined = not yet probed; null = native SHA-3 unavailable (BoringSSL/Electron). +let nativeHasher: NativeHasher | null | undefined; + +function detectNative(): NativeHasher | null { + try { + // Construct + digest once — Electron/BoringSSL throws here, not lazily. + createHash("sha3-512").update(Buffer.alloc(0)).digest("hex"); + return (data) => createHash("sha3-512").update(data).digest("hex"); + } catch { + return null; + } +} + +/** + * SHA3-512 hex digest that works on every runtime: native (OpenSSL) where + * available, pure-JS Keccak fallback where the crypto backend lacks SHA-3 + * (Electron/BoringSSL — #5531). Capability is probed once and cached. + */ +export function sha3_512Hex(input: string | Uint8Array): string { + const data = toBytes(input); + if (nativeHasher === undefined) nativeHasher = detectNative(); + if (nativeHasher) { + try { + return nativeHasher(data); + } catch { + nativeHasher = null; // became unavailable mid-flight — defensive + } + } + return sha3_512HexJs(data); +} + +/** Test seam: force the native capability state (null = simulate BoringSSL, undefined = re-probe). */ +export function __setSha3NativeForTesting(state: NativeHasher | null | undefined): void { + nativeHasher = state; +} diff --git a/open-sse/utils/streamHandler.ts b/open-sse/utils/streamHandler.ts index b1ca7dfe6a..720f361708 100644 --- a/open-sse/utils/streamHandler.ts +++ b/open-sse/utils/streamHandler.ts @@ -219,6 +219,7 @@ export function createStreamController({ const abortController = new AbortController(); const startTime = Date.now(); let disconnected = false; + let clientTerminalSeen = false; let pendingRequestCleared = false; let cleanupClientAbortSignal: (() => void) | null = null; @@ -274,6 +275,10 @@ export function createStreamController({ // Call when client disconnects handleDisconnect: (reason = "client_closed") => { if (disconnected) return; + if (clientTerminalSeen) { + controller.handleComplete(); + return; + } disconnected = true; cleanupClientAbortListener(); @@ -297,6 +302,10 @@ export function createStreamController({ logStream("complete"); }, + markClientTerminalSeen: () => { + clientTerminalSeen = true; + }, + // Call on error handleError: (error: unknown) => { cleanupClientAbortListener(); @@ -461,6 +470,9 @@ export function createDisconnectAwareStream(transformStream, streamController) { terminalTail, streamController.clientResponseFormat ); + if (clientTerminalSeen) { + streamController.markClientTerminalSeen?.(); + } }; return new ReadableStream( @@ -523,7 +535,11 @@ export function createDisconnectAwareStream(transformStream, streamController) { }, async cancel(reason) { - streamController.handleDisconnect(reason || "cancelled"); + if (clientTerminalSeen) { + streamController.handleComplete(); + } else { + streamController.handleDisconnect(reason || "cancelled"); + } await Promise.allSettled([reader.cancel(reason), writer.abort(reason)]); }, }, diff --git a/package-lock.json b/package-lock.json index 7a4d26e503..b3be55e1f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "3.8.41", + "version": "3.8.42", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "3.8.41", + "version": "3.8.42", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -63,7 +63,7 @@ "pino": "^10.3.1", "pino-abstract-transport": "^3.0.0", "pino-pretty": "^13.1.3", - "playwright": "1.61.0", + "playwright": "1.61.1", "react": "19.2.7", "react-dom": "19.2.7", "react-is": "^19.2.6", @@ -100,7 +100,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/better-sqlite3": "^7.6.13", - "@types/bun": "*", + "@types/bun": "latest", "@types/node": "^26.0.0", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", @@ -6203,38 +6203,6 @@ "node": ">=18" } }, - "node_modules/@playwright/test/node_modules/playwright": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", - "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.61.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/@playwright/test/node_modules/playwright-core": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", - "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -22991,12 +22959,12 @@ "optional": true }, "node_modules/playwright": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", - "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.61.0" + "playwright-core": "1.61.1" }, "bin": { "playwright": "cli.js" @@ -23009,9 +22977,9 @@ } }, "node_modules/playwright-core": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", - "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -28577,7 +28545,7 @@ }, "open-sse": { "name": "@omniroute/open-sse", - "version": "3.8.41", + "version": "3.8.42", "dependencies": { "@toon-format/toon": "^2.3.0", "safe-regex": "^2.1.1" diff --git a/package.json b/package.json index bb879518b8..ca8a21c4ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "3.8.41", + "version": "3.8.42", "description": "Unified AI router with 160+ providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.", "type": "module", "bin": { @@ -257,7 +257,7 @@ "pino": "^10.3.1", "pino-abstract-transport": "^3.0.0", "pino-pretty": "^13.1.3", - "playwright": "1.61.0", + "playwright": "1.61.1", "react": "19.2.7", "react-dom": "19.2.7", "react-is": "^19.2.6", diff --git a/scripts/build/pack-artifact-policy.ts b/scripts/build/pack-artifact-policy.ts index ba0451059e..6b0cb27a7e 100644 --- a/scripts/build/pack-artifact-policy.ts +++ b/scripts/build/pack-artifact-policy.ts @@ -47,6 +47,11 @@ export const APP_STAGING_ALLOWED_EXACT_PATHS: string[] = [ "scripts/dev/tls-options.mjs", "server.js", "server-ws.mjs", + // #5452: dist/tls-options.mjs is copied by assembleStandalone (EXTRA_MODULE_ENTRIES) + // and imported by dist/server-ws.mjs for opt-in native HTTPS/TLS (#5361). Without + // this bare entry the prepublish prune (Step 10.7) deletes it → `omniroute serve` + // crashes with ERR_MODULE_NOT_FOUND (regressed in the published 3.8.41 tarball). + "tls-options.mjs", "webdav-handler.mjs", ]; @@ -144,6 +149,9 @@ export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [ "dist/responses-ws-proxy.mjs", "dist/peer-stamp.mjs", "dist/http-method-guard.cjs", + // #5452: regression guard — make check:pack-artifact fail loudly if the TLS + // opt-in sidecar (imported by dist/server-ws.mjs) ever vanishes from the tarball. + "dist/tls-options.mjs", "dist/webdav-handler.mjs", "bin/cli/program.mjs", "bin/mcp-server.mjs", diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index ca3a4e105f..f27a243e81 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -46,6 +46,7 @@ import { normalizeIntelligentRoutingConfig, } from "@/lib/combos/intelligentRouting"; import { resolveServerErrorMessage } from "@/lib/api/serverErrorMessage"; +import { withDashboardCsrfHeader } from "@/shared/utils/dashboardCsrf"; import { useTranslations } from "next-intl"; const ModelSelectModal = dynamic(() => import("@/shared/components/ModelSelectModal"), { @@ -866,7 +867,7 @@ export default function CombosPage() { try { const res = await fetch("/api/combos/test", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: await withDashboardCsrfHeader({ "Content-Type": "application/json" }), body: JSON.stringify({ comboName: combo.name }), }); const data = await res.json(); diff --git a/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx b/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx index 7d4ef42403..76220671a8 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx @@ -19,6 +19,8 @@ import { compatibleProviderSupportsModelImport, getCompatibleFallbackModels, } from "@/lib/providers/managedAvailableModels"; +import { getProviderServiceKinds } from "@/lib/providers/serviceKindIndex"; +import { providerLacksModelListing } from "@/lib/providers/modelListingCapability"; import { normalizeModelCatalogSource } from "@/shared/utils/modelCatalogSearch"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import useEmailPrivacyStore from "@/store/emailPrivacyStore"; @@ -79,7 +81,17 @@ export default function ProviderDetailPageClient() { isAnthropicCompatibleProvider(providerId) && !isClaudeCodeCompatibleProvider(providerId); const isCompatible = isOpenAICompatible || isAnthropicCompatible || isCcCompatible; const isAnthropicProtocolCompatible = isAnthropicCompatible || isCcCompatible; - const isSearchProvider = providerId.endsWith("-search"); + // #5420: hide model listing for tool-only providers (web search / web fetch), + // not just `-search`-suffixed ids. Declared serviceKinds come from the static + // provider catalog (e.g. firecrawl → ["webFetch"]); compatible providers resolve + // to null here and fall through to the empty-kinds check (model listing stays on). + const declaredServiceKinds = ( + resolveDashboardProviderInfo(providerId) as { serviceKinds?: readonly string[] } | null + )?.serviceKinds; + const isSearchProvider = providerLacksModelListing( + providerId, + getProviderServiceKinds(providerId, declaredServiceKinds) + ); // ── Phase 1f hooks ──────────────────────────────────────────────────────── const { diff --git a/src/app/(dashboard)/dashboard/providers/[id]/__tests__/useModelVisibilityHandlers.test.tsx b/src/app/(dashboard)/dashboard/providers/[id]/__tests__/useModelVisibilityHandlers.test.tsx index eb5a42a23a..6dce9fa907 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/__tests__/useModelVisibilityHandlers.test.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/__tests__/useModelVisibilityHandlers.test.tsx @@ -3,6 +3,8 @@ import React, { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { DASHBOARD_CSRF_HEADER } from "@/shared/constants/dashboardCsrf"; +import { __resetDashboardCsrfTokenForTests } from "@/shared/utils/dashboardCsrf"; import { useModelVisibilityHandlers, type UseModelVisibilityHandlersReturn, @@ -70,6 +72,7 @@ beforeEach(() => { ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } ).IS_REACT_ACT_ENVIRONMENT = true; + __resetDashboardCsrfTokenForTests(); vi.stubGlobal("fetch", vi.fn()); vi.clearAllMocks(); }); @@ -91,14 +94,23 @@ describe("useModelVisibilityHandlers", () => { it("does not hide a model when a single-model test fails", async () => { const fetchMock = vi.mocked(fetch); - fetchMock.mockResolvedValueOnce({ - ok: false, - json: () => - Promise.resolve({ - status: "error", - error: "model unavailable", - }), - } as Response); + fetchMock + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + token: "csrf-token", + expiresAt: new Date(Date.now() + 60_000).toISOString(), + }), + } as Response) + .mockResolvedValueOnce({ + ok: false, + json: () => + Promise.resolve({ + status: "error", + error: "model unavailable", + }), + } as Response); const hook = renderHook(); @@ -108,11 +120,15 @@ describe("useModelVisibilityHandlers", () => { .onTestModel("claude-opus-4-8", "anthropic-compatible-cc-test/claude-opus-4-8"); }); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith( + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock).toHaveBeenNthCalledWith(1, "/api/auth/csrf", expect.any(Object)); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, "/api/models/test", expect.objectContaining({ method: "POST" }) ); + const [, modelTestInit] = fetchMock.mock.calls[1]; + expect((modelTestInit?.headers as Headers).get(DASHBOARD_CSRF_HEADER)).toBe("csrf-token"); expect( fetchMock.mock.calls.some(([url]) => String(url).startsWith("/api/provider-models")) ).toBe(false); diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx index e3dda0a620..4398a114e4 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx @@ -19,6 +19,7 @@ import { normalizeModelCatalogSource, } from "@/shared/utils/modelCatalogSearch"; import { useNotificationStore } from "@/store/notificationStore"; +import { withDashboardCsrfHeader } from "@/shared/utils/dashboardCsrf"; import { buildCompatMap, providerText, @@ -158,7 +159,7 @@ export default function PassthroughModelsSection({ >; } = await fetch("/api/models/test-all", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: await withDashboardCsrfHeader({ "Content-Type": "application/json" }), // Bug #3610 fix 2: pass autoHideFailed so the server persists the hide body: JSON.stringify( buildPassthroughTestBody({ diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx index 3d42adeacc..157829e875 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx @@ -97,7 +97,7 @@ export default function AddApiKeyModal({ }[commandCodeAuthState.phase] : null; const [formData, setFormData] = useState({ - name: "", + name: "main", // #5421: required field; default resists autofill garbage (was "" → "wiw") apiKey: "", defaultModel: "", priority: 1, diff --git a/src/app/(dashboard)/dashboard/providers/[id]/hooks/modelImportWarning.ts b/src/app/(dashboard)/dashboard/providers/[id]/hooks/modelImportWarning.ts new file mode 100644 index 0000000000..4422d216db --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/modelImportWarning.ts @@ -0,0 +1,17 @@ +/** + * modelImportWarning — pure helper (no React/store deps) so it is unit-testable in isolation. + * + * The model-import route (`/api/providers/[id]/models`) returns a `warning` field when it falls + * back to the cached/local catalog (e.g. the provider's `/models` endpoint was unreachable — + * "API unavailable — using local catalog"). The import hook previously read only `models`/`error`, + * so the fallback was silent: the user saw imported models with no indication they came from the + * local catalog instead of the live API (#5428, #5429, #5431). Returns the warning string to + * surface as a log line, or null when the response carries no usable warning. + */ +export function extractImportWarning(data: unknown): string | null { + if (data && typeof data === "object" && "warning" in data) { + const warning = (data as { warning?: unknown }).warning; + if (typeof warning === "string" && warning.trim()) return warning; + } + return null; +} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelImportHandlers.ts b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelImportHandlers.ts index 1b3e327663..2ea348bcf5 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelImportHandlers.ts +++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelImportHandlers.ts @@ -16,6 +16,7 @@ import React, { useState } from "react"; import type { ProviderMessageTranslator } from "../providerPageHelpers"; import { useNotificationStore } from "@/store/notificationStore"; +import { extractImportWarning } from "./modelImportWarning"; type NotifyStore = ReturnType; @@ -36,7 +37,11 @@ export interface UseModelImportHandlersParams { models: Array<{ id: string; name?: string }>; modelMeta: { customModels: Array<{ id: string }>; modelCompatOverrides?: unknown[] }; modelAliases: Record; - connections: Array<{ id?: string; isActive?: boolean; providerSpecificData?: Record }>; + connections: Array<{ + id?: string; + isActive?: boolean; + providerSpecificData?: Record; + }>; isFreeNoAuth: boolean; handleSetAlias: (modelId: string, alias: string, providerAlias: string) => Promise; fetchAliases: () => Promise; @@ -128,6 +133,7 @@ export function useModelImportHandlers({ return; } const fetchedModels = data.models || []; + const importWarning = extractImportWarning(data); if (fetchedModels.length === 0) { setImportProgress((prev) => ({ ...prev, @@ -151,7 +157,10 @@ export function useModelImportHandlers({ ...prev, phase: "done", status: t("allModelsAlreadyImported") || "All models already imported", - logs: [t("noNewModelsToImport") || "No new models to import"], + logs: [ + ...(importWarning ? [importWarning] : []), + t("noNewModelsToImport") || "No new models to import", + ], importedCount: 0, total: 0, current: 0, @@ -166,6 +175,7 @@ export function useModelImportHandlers({ current: 0, status: t("importingModelsProgress", { current: 0, total: newModels.length }), logs: [ + ...(importWarning ? [importWarning] : []), t("foundModelsStartingImport", { count: newModels.length }), ...(newModels.length < fetchedModels.length ? [ diff --git a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts index e9283d1b77..7f8f904e89 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts +++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts @@ -31,6 +31,7 @@ import { type CompatByProtocolMap, } from "../providerPageHelpers"; import { useNotificationStore } from "@/store/notificationStore"; +import { withDashboardCsrfHeader } from "@/shared/utils/dashboardCsrf"; type NotifyStore = ReturnType; @@ -290,7 +291,7 @@ export function useModelVisibilityHandlers({ try { const res = await fetch("/api/models/test", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: await withDashboardCsrfHeader({ "Content-Type": "application/json" }), body: JSON.stringify({ providerId: selectedConnection?.provider || providerNode?.id || providerId, modelId: fullModel, @@ -356,7 +357,7 @@ export function useModelVisibilityHandlers({ >; } = await fetch("/api/models/test-all", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: await withDashboardCsrfHeader({ "Content-Type": "application/json" }), body: JSON.stringify({ providerId: providerId, connectionId: selectedConnection?.id, diff --git a/src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingApi.ts b/src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingApi.ts index ba880d561f..3fe1bcf324 100644 --- a/src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingApi.ts +++ b/src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingApi.ts @@ -4,7 +4,7 @@ import { createProviderNodeSchema, createProviderSchema, validateProviderApiKeySchema, -} from "@/shared/validation/schemas"; +} from "@/shared/validation/schemas/provider"; export type OnboardingConnection = { id: string; diff --git a/src/app/(dashboard)/dashboard/providers/new/page.tsx b/src/app/(dashboard)/dashboard/providers/new/page.tsx index 819133ea07..fa174a49cd 100644 --- a/src/app/(dashboard)/dashboard/providers/new/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/new/page.tsx @@ -1,5 +1,11 @@ -import { redirect } from "next/navigation"; +import ProviderOnboardingWizard from "../components/onboarding/ProviderOnboardingWizard"; +// "Add Provider" / onboarding route. The buttons in providers/page.tsx and the +// dashboard widgets navigate here (#5427). This used to be a redirect stub that +// bounced back to /dashboard/providers, so the wizard silently never opened and +// the fully-built ProviderOnboardingWizard stayed orphaned. Render it directly — +// auth is enforced centrally by the (dashboard) DashboardLayout, same as the +// sibling [id] and index routes, so no per-page guard is needed here. export default function NewProviderPage() { - redirect("/dashboard/providers"); + return ; } diff --git a/src/app/(dashboard)/dashboard/providers/page.tsx b/src/app/(dashboard)/dashboard/providers/page.tsx index 93fe0c14ad..466b3e53d3 100644 --- a/src/app/(dashboard)/dashboard/providers/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/page.tsx @@ -28,6 +28,7 @@ import { import type { ProviderEntry } from "./providerPageUtils"; import { readProviderDisplayModePreference, + shouldSyncProviderDisplayMode, writeProviderDisplayModePreference, type ProviderDisplayMode, } from "./providerPageStorage"; @@ -263,21 +264,21 @@ export default function ProvidersPage() { }, []); useEffect(() => { - if (!displayModePreferenceReady) return; + if (!shouldSyncProviderDisplayMode(displayModePreferenceReady, loading)) return; const storedDisplayMode = connections.length === 0 && providerDisplayMode === "configured" ? "all" : providerDisplayMode; writeProviderDisplayModePreference(storedDisplayMode); - }, [connections.length, displayModePreferenceReady, providerDisplayMode]); + }, [connections.length, displayModePreferenceReady, providerDisplayMode, loading]); useEffect(() => { - if (!displayModePreferenceReady) return; + if (!shouldSyncProviderDisplayMode(displayModePreferenceReady, loading)) return; if (connections.length === 0 && providerDisplayMode === "configured") { setProviderDisplayMode("all"); } - }, [connections.length, displayModePreferenceReady, providerDisplayMode]); + }, [connections.length, displayModePreferenceReady, providerDisplayMode, loading]); const fetchOauthEnvRepairStatus = useCallback(async () => { try { diff --git a/src/app/(dashboard)/dashboard/providers/providerPageStorage.ts b/src/app/(dashboard)/dashboard/providers/providerPageStorage.ts index 92ad2cf1ad..1806504741 100644 --- a/src/app/(dashboard)/dashboard/providers/providerPageStorage.ts +++ b/src/app/(dashboard)/dashboard/providers/providerPageStorage.ts @@ -86,3 +86,14 @@ export function writeProviderDisplayModePreference( storage.setItem(PROVIDER_DISPLAY_MODE_STORAGE_KEY, mode); } + +/** + * Gate for the provider display-mode persistence effects. They must NOT run while the + * connections fetch is still in flight (`loading`), or they would coerce a saved + * "configured" preference to "all" against an empty connections list before the real + * data arrives — silently dropping the user's filter across reloads (#5510). Returns + * true only once the stored preference has been read (`ready`) AND loading has settled. + */ +export function shouldSyncProviderDisplayMode(ready: boolean, loading: boolean): boolean { + return ready && !loading; +} diff --git a/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx b/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx index 60e3ef473e..a68426f21d 100644 --- a/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx @@ -416,38 +416,20 @@ export default function CompressionSettingsTab() { config.defaultMode !== "lite" && config.cavemanConfig && (
-
-
-

- {t("compressionCavemanConfig")} -

-

- {t("compressionCavemanConfigDesc")} -

-
- + {/* Engine on/off is owned by the single-source panel (/dashboard/context/settings): + the panel's `engines.caveman.enabled` is authoritative (planResolution.ts). This tab + keeps only the advanced caveman tuning the panel does not expose. */} +
+

+ {t("compressionCavemanConfig")} +

+

+ {t("compressionCavemanConfigDesc")} {t("compressionCavemanPanelHint")}{" "} + /dashboard/context/settings +

- {config.cavemanConfig.enabled && ( - <> + <>

{t("compressionRoles")}

@@ -534,8 +516,7 @@ export default function CompressionSettingsTab() { className="w-full min-h-[80px] px-3 py-2 text-sm rounded-lg border border-border bg-surface text-text-main font-mono resize-y" />
- - )} +
)} diff --git a/src/app/api/auth/csrf/route.ts b/src/app/api/auth/csrf/route.ts new file mode 100644 index 0000000000..88fae6c3a2 --- /dev/null +++ b/src/app/api/auth/csrf/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; + +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; +import { issueDashboardCsrfToken } from "@/server/authz/csrf"; + +export async function GET(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + + const issued = issueDashboardCsrfToken(request); + return NextResponse.json(issued ?? { token: null, expiresAt: null }, { + headers: { "Cache-Control": "no-store" }, + }); +} diff --git a/src/app/api/db-backups/import/route.ts b/src/app/api/db-backups/import/route.ts index b118e83d7b..8186abe23b 100644 --- a/src/app/api/db-backups/import/route.ts +++ b/src/app/api/db-backups/import/route.ts @@ -5,7 +5,12 @@ import os from "os"; import { getDbInstance, resetDbInstance, SQLITE_FILE } from "@/lib/db/core"; import { openDatabaseAsync } from "@/lib/db/adapters/driverFactory"; import type { SqliteAdapter } from "@/lib/db/adapters/types"; -import { backupDbFile, getTableNamesFromAdapter, countImportedRows } from "@/lib/db/backup"; +import { + backupDbFile, + getTableNamesFromAdapter, + countImportedRows, + unlinkFileWithRetry, +} from "@/lib/db/backup"; import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth"; import { getSettings } from "@/lib/db/settings"; import { setSystemPromptConfig } from "@omniroute/open-sse/services/systemPrompt.ts"; @@ -167,11 +172,12 @@ export async function POST(request: Request) { `${SQLITE_FILE}-shm`, `${SQLITE_FILE}-journal`, ]; + // Delete with EBUSY/EPERM retry: after resetDbInstance() the OS may still + // hold the SQLite file handle for a moment (Windows mmap / antivirus), so a + // plain unlink races to EBUSY (#5406). Mirror the restore path's helper. for (const filePath of sqliteFilesToReplace) { if (!filePath) continue; - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - } + await unlinkFileWithRetry(filePath); } // Copy imported file over current DB diff --git a/src/app/api/local/redis/redisRuntime.ts b/src/app/api/local/redis/redisRuntime.ts new file mode 100644 index 0000000000..5417b3db8f --- /dev/null +++ b/src/app/api/local/redis/redisRuntime.ts @@ -0,0 +1,46 @@ +import { NextResponse } from "next/server"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +export const REDIS_CONTAINER_NAME = process.env.OMNIROUTE_REDIS_CONTAINER_NAME || "omniroute-redis"; + +export const RUNTIME_PREFERENCE = ["podman", "docker"] as const; + +type ExecFileAsync = ( + file: string, + args: readonly string[], + options: { timeout: number } +) => Promise<{ stdout: string; stderr: string }>; + +const execFileAsync = promisify(execFile) as ExecFileAsync; + +export async function detectRedisContainerRuntime( + runCommand: ExecFileAsync = execFileAsync +): Promise { + for (const candidate of RUNTIME_PREFERENCE) { + try { + await runCommand(candidate, ["--version"], { timeout: 3000 }); + return candidate; + } catch { + // try next + } + } + return null; +} + +export function redisRuntimeUnavailableResponse() { + return NextResponse.json( + { ok: false, error: "No container runtime (podman or docker) found on PATH" }, + { status: 503 } + ); +} + +export async function runRedisRuntimeCommand( + runtime: string, + args: readonly string[], + timeout: number, + runCommand: ExecFileAsync = execFileAsync +) { + const { stdout, stderr } = await runCommand(runtime, args, { timeout }); + return { stdout: stdout.trim(), stderr: stderr.trim() }; +} diff --git a/src/app/api/local/redis/start/route.ts b/src/app/api/local/redis/start/route.ts index 5bf4d5e9cb..671d255f8d 100644 --- a/src/app/api/local/redis/start/route.ts +++ b/src/app/api/local/redis/start/route.ts @@ -1,42 +1,27 @@ import { NextResponse } from "next/server"; -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; import { isLocalRequestAllowed } from "@/lib/security/localEndpoints"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; -const execFileAsync = promisify(execFile); +import { + REDIS_CONTAINER_NAME, + detectRedisContainerRuntime, + redisRuntimeUnavailableResponse, + runRedisRuntimeCommand, +} from "../redisRuntime"; -const CONTAINER_NAME = process.env.OMNIROUTE_REDIS_CONTAINER_NAME || "omniroute-redis"; const HOST_PORT = process.env.OMNIROUTE_REDIS_HOST_PORT || "6379"; const IMAGE = process.env.OMNIROUTE_REDIS_IMAGE || "docker.io/redis:7-alpine"; -const RUNTIME_PREFERENCE = ["podman", "docker"]; - -async function detectRuntime(): Promise { - for (const candidate of RUNTIME_PREFERENCE) { - try { - await execFileAsync(candidate, ["--version"], { timeout: 3000 }); - return candidate; - } catch { - // try next - } - } - return null; -} - export async function POST() { const guard = isLocalRequestAllowed(); if (!guard.allowed) { return NextResponse.json({ error: guard.reason }, { status: 403 }); } - const runtime = await detectRuntime(); + const runtime = await detectRedisContainerRuntime(); if (!runtime) { - return NextResponse.json( - { ok: false, error: "No container runtime (podman or docker) found on PATH" }, - { status: 503 } - ); + return redisRuntimeUnavailableResponse(); } try { @@ -48,15 +33,22 @@ export async function POST() { "run", "-d", "--name", - CONTAINER_NAME, + REDIS_CONTAINER_NAME, "-p", `${HOST_PORT}:6379`, "--restart", "unless-stopped", IMAGE, ]; - const { stdout, stderr } = await execFileAsync(runtime, args, { timeout: 30_000 }); - return NextResponse.json({ ok: true, runtime, name: CONTAINER_NAME, port: HOST_PORT, stdout: stdout.trim(), stderr: stderr.trim() }); + const { stdout, stderr } = await runRedisRuntimeCommand(runtime, args, 30_000); + return NextResponse.json({ + ok: true, + runtime, + name: REDIS_CONTAINER_NAME, + port: HOST_PORT, + stdout, + stderr, + }); } catch (err) { // Hard Rule #12: never put a raw execFile error (command line + paths) in the body. return NextResponse.json( @@ -64,4 +56,4 @@ export async function POST() { { status: 500 } ); } -} \ No newline at end of file +} diff --git a/src/app/api/local/redis/stop/route.ts b/src/app/api/local/redis/stop/route.ts index 6f7f73c59f..4ce9fd224a 100644 --- a/src/app/api/local/redis/stop/route.ts +++ b/src/app/api/local/redis/stop/route.ts @@ -1,27 +1,14 @@ import { NextResponse } from "next/server"; -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; import { isLocalRequestAllowed } from "@/lib/security/localEndpoints"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; -const execFileAsync = promisify(execFile); - -const CONTAINER_NAME = process.env.OMNIROUTE_REDIS_CONTAINER_NAME || "omniroute-redis"; - -const RUNTIME_PREFERENCE = ["podman", "docker"]; - -async function detectRuntime(): Promise { - for (const candidate of RUNTIME_PREFERENCE) { - try { - await execFileAsync(candidate, ["--version"], { timeout: 3000 }); - return candidate; - } catch { - // try next - } - } - return null; -} +import { + REDIS_CONTAINER_NAME, + detectRedisContainerRuntime, + redisRuntimeUnavailableResponse, + runRedisRuntimeCommand, +} from "../redisRuntime"; export async function POST() { const guard = isLocalRequestAllowed(); @@ -29,17 +16,24 @@ export async function POST() { return NextResponse.json({ error: guard.reason }, { status: 403 }); } - const runtime = await detectRuntime(); + const runtime = await detectRedisContainerRuntime(); if (!runtime) { - return NextResponse.json( - { ok: false, error: "No container runtime (podman or docker) found on PATH" }, - { status: 503 } - ); + return redisRuntimeUnavailableResponse(); } try { - const { stdout, stderr } = await execFileAsync(runtime, ["stop", CONTAINER_NAME], { timeout: 15_000 }); - return NextResponse.json({ ok: true, runtime, name: CONTAINER_NAME, stdout: stdout.trim(), stderr: stderr.trim() }); + const { stdout, stderr } = await runRedisRuntimeCommand( + runtime, + ["stop", REDIS_CONTAINER_NAME], + 15_000 + ); + return NextResponse.json({ + ok: true, + runtime, + name: REDIS_CONTAINER_NAME, + stdout, + stderr, + }); } catch (err) { const rawMessage = err instanceof Error ? err.message : String(err); // exit code != 0 from `stop` typically means "not running" — surface that as ok=false but don't 500 @@ -52,4 +46,4 @@ export async function POST() { { status: 500 } ); } -} \ No newline at end of file +} diff --git a/src/app/api/providers/health-autopilot/actions/route.ts b/src/app/api/providers/health-autopilot/actions/route.ts index 4bff91fcd8..1cc58e5bae 100644 --- a/src/app/api/providers/health-autopilot/actions/route.ts +++ b/src/app/api/providers/health-autopilot/actions/route.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { executeProviderHealthAutopilotAction } from "@/lib/monitoring/providerHealthAutopilot"; +import { validateBrowserMutationOrigin } from "@/server/origin/publicOrigin"; import { validateBody } from "@/shared/validation/helpers"; const actionSchema = z.object({ @@ -28,6 +29,11 @@ export async function POST(request: Request) { const authError = await requireManagementAuth(request); if (authError) return authError; + const originVerdict = validateBrowserMutationOrigin(request); + if (!originVerdict.ok) { + return NextResponse.json({ error: { message: "Invalid request origin" } }, { status: 403 }); + } + try { let rawBody: unknown; try { diff --git a/src/app/api/services/9router/install/route.ts b/src/app/api/services/9router/install/route.ts index a2df024eef..2a11259c4a 100644 --- a/src/app/api/services/9router/install/route.ts +++ b/src/app/api/services/9router/install/route.ts @@ -1,37 +1,6 @@ -import { z } from "zod"; -import { install, InstallResult } from "@/lib/services/installers/ninerouter"; -import { InstallError } from "@/lib/services/installers/utils"; -import { createErrorResponse } from "@/lib/api/errorResponse"; -import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; - -const BodySchema = z.object({ - version: z.string().optional().default("latest"), -}); +import { install } from "@/lib/services/installers/ninerouter"; +import { handleServiceInstall } from "@/app/api/services/_shared/installRoute"; export async function POST(request: Request): Promise { - let body: unknown; - try { - body = request.body === null ? {} : await request.json(); - } catch { - return createErrorResponse({ status: 400, message: "Invalid JSON body" }); - } - - const parsed = BodySchema.safeParse(body); - if (!parsed.success) { - return createErrorResponse({ status: 400, message: parsed.error.message }); - } - - try { - const result: InstallResult = await install(parsed.data.version); - return Response.json({ ok: true, ...result }); - } catch (err) { - if (err instanceof InstallError) { - return createErrorResponse({ - status: err.httpStatus, - message: err.friendly, - }); - } - const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err)); - return createErrorResponse({ status: 500, message: msg }); - } + return handleServiceInstall(request, install); } diff --git a/src/app/api/services/_shared/installRoute.ts b/src/app/api/services/_shared/installRoute.ts new file mode 100644 index 0000000000..5d23725799 --- /dev/null +++ b/src/app/api/services/_shared/installRoute.ts @@ -0,0 +1,79 @@ +import { z } from "zod"; + +import { createErrorResponse } from "@/lib/api/errorResponse"; +import { InstallError, SERVICE_VERSION_PATTERN } from "@/lib/services/installers/utils"; +import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; + +export type ServiceInstallResult = { + installedVersion: string; + installPath: string; + durationMs: number; +}; + +export type ServiceInstaller = (version: string) => Promise; + +const installBodySchema = z.object({ + // Keep the version constrained by SERVICE_VERSION_PATTERN — the per-route schemas + // enforced this before the extraction (#5474); dropping it here would let strings + // like "../../malicious" reach the installer (#5495). + version: z + .string() + .regex(SERVICE_VERSION_PATTERN, "Invalid version: only letters, digits and . _ + - are allowed") + .optional() + .default("latest"), +}); + +export async function readServiceInstallVersion(request: Request): Promise< + | { + ok: true; + version: string; + } + | { + ok: false; + response: Response; + } +> { + let body: unknown; + try { + body = request.body === null ? {} : await request.json(); + } catch { + return { + ok: false, + response: createErrorResponse({ status: 400, message: "Invalid JSON body" }), + }; + } + + const parsed = installBodySchema.safeParse(body); + if (!parsed.success) { + return { + ok: false, + response: createErrorResponse({ status: 400, message: parsed.error.message }), + }; + } + + return { ok: true, version: parsed.data.version }; +} + +export async function handleServiceInstall( + request: Request, + install: ServiceInstaller +): Promise { + const parsed = await readServiceInstallVersion(request); + if (!parsed.ok) { + return parsed.response; + } + + try { + const result = await install(parsed.version); + return Response.json({ ok: true, ...result }); + } catch (err) { + if (err instanceof InstallError) { + return createErrorResponse({ + status: err.httpStatus, + message: err.friendly, + }); + } + const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err)); + return createErrorResponse({ status: 500, message: msg }); + } +} diff --git a/src/app/api/services/cliproxy/install/route.ts b/src/app/api/services/cliproxy/install/route.ts index 591489cc24..5b463fd856 100644 --- a/src/app/api/services/cliproxy/install/route.ts +++ b/src/app/api/services/cliproxy/install/route.ts @@ -1,34 +1,6 @@ -import { z } from "zod"; -import { install, InstallResult } from "@/lib/services/installers/cliproxy"; -import { InstallError } from "@/lib/services/installers/utils"; -import { createErrorResponse } from "@/lib/api/errorResponse"; -import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; - -const BodySchema = z.object({ - version: z.string().optional().default("latest"), -}); +import { install } from "@/lib/services/installers/cliproxy"; +import { handleServiceInstall } from "@/app/api/services/_shared/installRoute"; export async function POST(request: Request): Promise { - let body: unknown; - try { - body = request.body === null ? {} : await request.json(); - } catch { - return createErrorResponse({ status: 400, message: "Invalid JSON body" }); - } - - const parsed = BodySchema.safeParse(body); - if (!parsed.success) { - return createErrorResponse({ status: 400, message: parsed.error.message }); - } - - try { - const result: InstallResult = await install(parsed.data.version); - return Response.json({ ok: true, ...result }); - } catch (err) { - if (err instanceof InstallError) { - return createErrorResponse({ status: err.httpStatus, message: err.friendly }); - } - const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err)); - return createErrorResponse({ status: 500, message: msg }); - } + return handleServiceInstall(request, install); } diff --git a/src/app/api/settings/proxies/route.ts b/src/app/api/settings/proxies/route.ts index 4bb156f9b6..62abcaf344 100644 --- a/src/app/api/settings/proxies/route.ts +++ b/src/app/api/settings/proxies/route.ts @@ -1,39 +1,20 @@ +import { listProxies } from "@/lib/localDb"; import { - createProxy, - createProxyAndAssign, - deleteProxyById, - getProxyById, - getProxyWhereUsed, - listProxies, - updateProxy, - updateProxyAndAssign, -} from "@/lib/localDb"; -import { createProxyRegistrySchema, updateProxyRegistrySchema } from "@/shared/validation/schemas"; -import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; -import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse"; + handleProxyCreate, + handleProxyDelete, + handleProxyUpdate, + resolveProxyLookupResponse, +} from "@/lib/api/proxyRegistryRouteHandlers"; +import { createErrorResponseFromUnknown } from "@/lib/api/errorResponse"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; -import { clearDispatcherCache } from "@omniroute/open-sse/utils/proxyDispatcher"; export async function GET(request: Request) { const authError = await requireManagementAuth(request); if (authError) return authError; try { const { searchParams } = new URL(request.url); - const id = searchParams.get("id"); - const whereUsed = searchParams.get("whereUsed") === "1"; - - if (id && whereUsed) { - const usage = await getProxyWhereUsed(id); - return Response.json(usage); - } - - if (id) { - const proxy = await getProxyById(id, { includeSecrets: false }); - if (!proxy) { - return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" }); - } - return Response.json(proxy); - } + const lookupResponse = await resolveProxyLookupResponse(searchParams, "whereUsed"); + if (lookupResponse) return lookupResponse; const proxies = await listProxies({ includeSecrets: false }); // #3508: expose the SOCKS5 feature flag at runtime so the dashboard reflects the live @@ -55,112 +36,17 @@ export async function GET(request: Request) { export async function POST(request: Request) { const authError = await requireManagementAuth(request); if (authError) return authError; - let rawBody: unknown; - try { - rawBody = await request.json(); - } catch { - return createErrorResponse({ - status: 400, - message: "Invalid JSON body", - type: "invalid_request", - }); - } - - try { - const validation = validateBody(createProxyRegistrySchema, rawBody); - if (isValidationFailure(validation)) { - return createErrorResponse({ - status: 400, - message: validation.error.message, - details: validation.error.details, - type: "invalid_request", - }); - } - - const { assignment, ...proxyFields } = validation.data; - if (assignment) { - const result = await createProxyAndAssign(proxyFields, assignment); - clearDispatcherCache(); - return Response.json({ ...result.proxy, assignment: result.assignment }, { status: 201 }); - } - - const created = await createProxy(proxyFields); - return Response.json(created, { status: 201 }); - } catch (error) { - return createErrorResponseFromUnknown(error, "Failed to create proxy"); - } + return handleProxyCreate(request); } export async function PATCH(request: Request) { const authError = await requireManagementAuth(request); if (authError) return authError; - let rawBody: unknown; - try { - rawBody = await request.json(); - } catch { - return createErrorResponse({ - status: 400, - message: "Invalid JSON body", - type: "invalid_request", - }); - } - - try { - const validation = validateBody(updateProxyRegistrySchema, rawBody); - if (isValidationFailure(validation)) { - return createErrorResponse({ - status: 400, - message: validation.error.message, - details: validation.error.details, - type: "invalid_request", - }); - } - - const { id, assignment, ...changes } = validation.data; - if (assignment) { - const result = await updateProxyAndAssign(id, changes, assignment); - if (!result?.proxy) { - return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" }); - } - - clearDispatcherCache(); - return Response.json({ ...result.proxy, assignment: result.assignment }); - } - - const updated = await updateProxy(id, changes); - if (!updated) { - return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" }); - } - - return Response.json(updated); - } catch (error) { - return createErrorResponseFromUnknown(error, "Failed to update proxy"); - } + return handleProxyUpdate(request); } export async function DELETE(request: Request) { const authError = await requireManagementAuth(request); if (authError) return authError; - try { - const { searchParams } = new URL(request.url); - const id = searchParams.get("id"); - const force = searchParams.get("force") === "1"; - - if (!id) { - return createErrorResponse({ - status: 400, - message: "id is required", - type: "invalid_request", - }); - } - - const deleted = await deleteProxyById(id, { force }); - if (!deleted) { - return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" }); - } - - return Response.json({ success: true }); - } catch (error) { - return createErrorResponseFromUnknown(error, "Failed to delete proxy"); - } + return handleProxyDelete(request); } diff --git a/src/app/api/v1/_shared/mediaGenerationRoute.ts b/src/app/api/v1/_shared/mediaGenerationRoute.ts new file mode 100644 index 0000000000..ccc5a8803b --- /dev/null +++ b/src/app/api/v1/_shared/mediaGenerationRoute.ts @@ -0,0 +1,133 @@ +import { errorResponse } from "@omniroute/open-sse/utils/error.ts"; +import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; + +import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta"; +import { calculateModalCost } from "@/lib/usage/costCalculator"; +import { generateRequestId } from "@/shared/utils/requestId"; +import { toJsonErrorPayload } from "@/shared/utils/upstreamError"; +import { v1ImageGenerationSchema } from "@/shared/validation/schemas"; +import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; + +type MediaModelListEntry = { + id: string; + provider: string; +}; + +type MediaGenerationResult = + | { success: true; data: unknown } + | { success: false; error: unknown; status: number }; + +type MediaGenerationBody = { + model: string; + prompt?: unknown; + duration?: unknown; +} & Record; + +type ValidatedMediaGenerationBody = + | { ok: true; body: MediaGenerationBody } + | { ok: false; response: Response }; + +export function mediaGenerationOptionsResponse() { + return new Response(null, { + headers: { + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "*", + }, + }); +} + +export function mediaGenerationModelListResponse( + models: MediaModelListEntry[], + type: "music" | "video" +) { + return new Response( + JSON.stringify({ + object: "list", + data: models.map((m) => ({ + id: m.id, + object: "model", + created: Math.floor(Date.now() / 1000), + owned_by: m.provider, + type, + })), + }), + { + headers: { "Content-Type": "application/json" }, + } + ); +} + +export async function readMediaGenerationBody( + request: Request, + log: { warn: (scope: string, message: string) => void }, + logScope: string +): Promise { + let rawBody: unknown; + try { + rawBody = await request.json(); + } catch { + log.warn(logScope, "Invalid JSON body"); + return { ok: false, response: errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body") }; + } + + const validation = validateBody(v1ImageGenerationSchema, rawBody); + if (isValidationFailure(validation)) { + return { + ok: false, + response: errorResponse(HTTP_STATUS.BAD_REQUEST, validation.error.message), + }; + } + + return { ok: true, body: validation.data as MediaGenerationBody }; +} + +export function promptRequiredResponse(body: { prompt?: unknown }) { + if (typeof body.prompt === "string" && body.prompt.trim().length > 0) { + return null; + } + + return errorResponse(HTTP_STATUS.BAD_REQUEST, "Prompt is required"); +} + +export async function successfulMediaGenerationResponse({ + result, + billingMode, + provider, + model, + startTime, + duration, +}: { + result: { data: unknown }; + billingMode: "audio" | "video"; + provider: string; + model: string; + startTime: number; + duration: unknown; +}) { + const seconds = Number(duration) || 0; + const costUsd = await calculateModalCost(billingMode, provider, model, { seconds }); + const headers = new Headers({ "Content-Type": "application/json" }); + attachOmniRouteMetaHeaders(headers, { + provider, + model, + costUsd, + latencyMs: Date.now() - startTime, + requestId: generateRequestId(), + }); + + return new Response(JSON.stringify(result.data), { + status: 200, + headers, + }); +} + +export function failedMediaGenerationResponse( + result: MediaGenerationResult, + fallbackMessage: string +) { + const errorPayload = toJsonErrorPayload(result.error, fallbackMessage); + return new Response(JSON.stringify(errorPayload), { + status: result.status, + headers: { "Content-Type": "application/json" }, + }); +} diff --git a/src/app/api/v1/batches/[id]/cancel/route.ts b/src/app/api/v1/batches/[id]/cancel/route.ts index 1966af8446..fec9aa39c9 100644 --- a/src/app/api/v1/batches/[id]/cancel/route.ts +++ b/src/app/api/v1/batches/[id]/cancel/route.ts @@ -2,37 +2,7 @@ import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors"; import { getBatch, updateBatch } from "@/lib/localDb"; import { NextResponse } from "next/server"; import { getApiKeyRequestScope } from "@/app/api/v1/_helpers/apiKeyScope"; - -function formatBatchResponse(batch: any) { - return { - id: batch.id, - object: "batch", - endpoint: batch.endpoint, - errors: batch.errors || null, - input_file_id: batch.inputFileId, - completion_window: batch.completionWindow, - status: batch.status, - output_file_id: batch.outputFileId || null, - error_file_id: batch.errorFileId || null, - created_at: batch.createdAt, - in_progress_at: batch.inProgressAt || null, - expires_at: batch.expiresAt || null, - finalizing_at: batch.finalizingAt || null, - completed_at: batch.completedAt || null, - failed_at: batch.failedAt || null, - expired_at: batch.expiredAt || null, - cancelling_at: batch.cancellingAt || null, - cancelled_at: batch.cancelledAt || null, - request_counts: { - total: batch.requestCountsTotal || 0, - completed: batch.requestCountsCompleted || 0, - failed: batch.requestCountsFailed || 0, - }, - metadata: batch.metadata || null, - model: batch.model || null, - usage: batch.usage || null, - }; -} +import { formatBatchResponse } from "../../formatBatchResponse"; export async function OPTIONS() { return handleCorsOptions(); diff --git a/src/app/api/v1/batches/[id]/route.ts b/src/app/api/v1/batches/[id]/route.ts index 7cca8d8cfc..a536902a78 100644 --- a/src/app/api/v1/batches/[id]/route.ts +++ b/src/app/api/v1/batches/[id]/route.ts @@ -2,37 +2,7 @@ import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors"; import { getBatch, deleteBatch } from "@/lib/localDb"; import { NextResponse } from "next/server"; import { getApiKeyRequestScope } from "@/app/api/v1/_helpers/apiKeyScope"; - -function formatBatchResponse(batch: any) { - return { - id: batch.id, - object: "batch", - endpoint: batch.endpoint, - errors: batch.errors || null, - input_file_id: batch.inputFileId, - completion_window: batch.completionWindow, - status: batch.status, - output_file_id: batch.outputFileId || null, - error_file_id: batch.errorFileId || null, - created_at: batch.createdAt, - in_progress_at: batch.inProgressAt || null, - expires_at: batch.expiresAt || null, - finalizing_at: batch.finalizingAt || null, - completed_at: batch.completedAt || null, - failed_at: batch.failedAt || null, - expired_at: batch.expiredAt || null, - cancelling_at: batch.cancellingAt || null, - cancelled_at: batch.cancelledAt || null, - request_counts: { - total: batch.requestCountsTotal || 0, - completed: batch.requestCountsCompleted || 0, - failed: batch.requestCountsFailed || 0, - }, - metadata: batch.metadata || null, - model: batch.model || null, - usage: batch.usage || null, - }; -} +import { formatBatchResponse } from "../formatBatchResponse"; export async function OPTIONS() { return handleCorsOptions(); diff --git a/src/app/api/v1/batches/formatBatchResponse.ts b/src/app/api/v1/batches/formatBatchResponse.ts new file mode 100644 index 0000000000..5fbc58bdef --- /dev/null +++ b/src/app/api/v1/batches/formatBatchResponse.ts @@ -0,0 +1,30 @@ +export function formatBatchResponse(batch: any) { + return { + id: batch.id, + object: "batch", + endpoint: batch.endpoint, + errors: batch.errors || null, + input_file_id: batch.inputFileId, + completion_window: batch.completionWindow, + status: batch.status, + output_file_id: batch.outputFileId || null, + error_file_id: batch.errorFileId || null, + created_at: batch.createdAt, + in_progress_at: batch.inProgressAt || null, + expires_at: batch.expiresAt || null, + finalizing_at: batch.finalizingAt || null, + completed_at: batch.completedAt || null, + failed_at: batch.failedAt || null, + expired_at: batch.expiredAt || null, + cancelling_at: batch.cancellingAt || null, + cancelled_at: batch.cancelledAt || null, + request_counts: { + total: batch.requestCountsTotal || 0, + completed: batch.requestCountsCompleted || 0, + failed: batch.requestCountsFailed || 0, + }, + metadata: batch.metadata || null, + model: batch.model || null, + usage: batch.usage || null, + }; +} diff --git a/src/app/api/v1/batches/route.ts b/src/app/api/v1/batches/route.ts index 9c75e03d7b..cc4a2bcd14 100644 --- a/src/app/api/v1/batches/route.ts +++ b/src/app/api/v1/batches/route.ts @@ -3,37 +3,7 @@ import { createBatch, getFile, listBatches, countBatches } from "@/lib/localDb"; import { v1BatchCreateSchema } from "@/shared/validation/schemas"; import { NextResponse } from "next/server"; import { getApiKeyRequestScope } from "@/app/api/v1/_helpers/apiKeyScope"; - -function formatBatchResponse(batch: any) { - return { - id: batch.id, - object: "batch", - endpoint: batch.endpoint, - errors: batch.errors || null, - input_file_id: batch.inputFileId, - completion_window: batch.completionWindow, - status: batch.status, - output_file_id: batch.outputFileId || null, - error_file_id: batch.errorFileId || null, - created_at: batch.createdAt, - in_progress_at: batch.inProgressAt || null, - expires_at: batch.expiresAt || null, - finalizing_at: batch.finalizingAt || null, - completed_at: batch.completedAt || null, - failed_at: batch.failedAt || null, - expired_at: batch.expiredAt || null, - cancelling_at: batch.cancellingAt || null, - cancelled_at: batch.cancelledAt || null, - request_counts: { - total: batch.requestCountsTotal || 0, - completed: batch.requestCountsCompleted || 0, - failed: batch.requestCountsFailed || 0, - }, - metadata: batch.metadata || null, - model: batch.model || null, - usage: batch.usage || null, - }; -} +import { formatBatchResponse } from "./formatBatchResponse"; export async function OPTIONS() { return handleCorsOptions(); diff --git a/src/app/api/v1/management/proxies/route.ts b/src/app/api/v1/management/proxies/route.ts index 6ab5e69655..29a49101d3 100644 --- a/src/app/api/v1/management/proxies/route.ts +++ b/src/app/api/v1/management/proxies/route.ts @@ -1,18 +1,12 @@ +import { listProxies } from "@/lib/localDb"; import { - createProxy, - createProxyAndAssign, - deleteProxyById, - getProxyById, - getProxyWhereUsed, - listProxies, - updateProxy, - updateProxyAndAssign, -} from "@/lib/localDb"; -import { createProxyRegistrySchema, updateProxyRegistrySchema } from "@/shared/validation/schemas"; -import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; -import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse"; + handleProxyCreate, + handleProxyDelete, + handleProxyUpdate, + resolveProxyLookupResponse, +} from "@/lib/api/proxyRegistryRouteHandlers"; +import { createErrorResponseFromUnknown } from "@/lib/api/errorResponse"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; -import { clearDispatcherCache } from "@omniroute/open-sse/utils/proxyDispatcher"; function toPagination(searchParams: URLSearchParams) { const limit = Math.max(1, Math.min(200, Number(searchParams.get("limit") || 50))); @@ -26,21 +20,8 @@ export async function GET(request: Request) { try { const { searchParams } = new URL(request.url); - const id = searchParams.get("id"); - const whereUsed = searchParams.get("where_used") === "1"; - - if (id && whereUsed) { - const usage = await getProxyWhereUsed(id); - return Response.json(usage); - } - - if (id) { - const proxy = await getProxyById(id, { includeSecrets: false }); - if (!proxy) { - return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" }); - } - return Response.json(proxy); - } + const lookupResponse = await resolveProxyLookupResponse(searchParams, "where_used"); + if (lookupResponse) return lookupResponse; const { limit, offset } = toPagination(searchParams); const items = await listProxies({ includeSecrets: false }); @@ -57,115 +38,17 @@ export async function GET(request: Request) { export async function POST(request: Request) { const authError = await requireManagementAuth(request); if (authError) return authError; - - let rawBody: unknown; - try { - rawBody = await request.json(); - } catch { - return createErrorResponse({ - status: 400, - message: "Invalid JSON body", - type: "invalid_request", - }); - } - - try { - const validation = validateBody(createProxyRegistrySchema, rawBody); - if (isValidationFailure(validation)) { - return createErrorResponse({ - status: 400, - message: validation.error.message, - details: validation.error.details, - type: "invalid_request", - }); - } - - const { assignment, ...proxyFields } = validation.data; - if (assignment) { - const result = await createProxyAndAssign(proxyFields, assignment); - clearDispatcherCache(); - return Response.json({ ...result.proxy, assignment: result.assignment }, { status: 201 }); - } - - const created = await createProxy(proxyFields); - return Response.json(created, { status: 201 }); - } catch (error) { - return createErrorResponseFromUnknown(error, "Failed to create proxy"); - } + return handleProxyCreate(request); } export async function PATCH(request: Request) { const authError = await requireManagementAuth(request); if (authError) return authError; - - let rawBody: unknown; - try { - rawBody = await request.json(); - } catch { - return createErrorResponse({ - status: 400, - message: "Invalid JSON body", - type: "invalid_request", - }); - } - - try { - const validation = validateBody(updateProxyRegistrySchema, rawBody); - if (isValidationFailure(validation)) { - return createErrorResponse({ - status: 400, - message: validation.error.message, - details: validation.error.details, - type: "invalid_request", - }); - } - - const { id, assignment, ...changes } = validation.data; - if (assignment) { - const result = await updateProxyAndAssign(id, changes, assignment); - if (!result?.proxy) { - return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" }); - } - - clearDispatcherCache(); - return Response.json({ ...result.proxy, assignment: result.assignment }); - } - - const updated = await updateProxy(id, changes); - if (!updated) { - return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" }); - } - - return Response.json(updated); - } catch (error) { - return createErrorResponseFromUnknown(error, "Failed to update proxy"); - } + return handleProxyUpdate(request); } export async function DELETE(request: Request) { const authError = await requireManagementAuth(request); if (authError) return authError; - - try { - const { searchParams } = new URL(request.url); - const id = searchParams.get("id"); - const force = searchParams.get("force") === "1"; - - if (!id) { - return createErrorResponse({ - status: 400, - message: "id is required", - type: "invalid_request", - }); - } - - const deleted = await deleteProxyById(id, { force }); - if (!deleted) { - return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" }); - } - - return Response.json({ success: true }); - } catch (error) { - return createErrorResponseFromUnknown(error, "Failed to delete proxy"); - } + return handleProxyDelete(request); } diff --git a/src/app/api/v1/music/generations/route.ts b/src/app/api/v1/music/generations/route.ts index f3b5f1054c..0382f376b4 100644 --- a/src/app/api/v1/music/generations/route.ts +++ b/src/app/api/v1/music/generations/route.ts @@ -1,11 +1,6 @@ import { handleMusicGeneration } from "@omniroute/open-sse/handlers/musicGeneration.ts"; import { withInjectionGuard } from "@/middleware/promptInjectionGuard"; -import { - getProviderCredentials, - clearRecoveredProviderState, - extractApiKey, - isValidApiKey, -} from "@/sse/services/auth"; +import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth"; import { parseMusicModel, getAllMusicModels, @@ -14,74 +9,47 @@ import { import { errorResponse } from "@omniroute/open-sse/utils/error.ts"; import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; import * as log from "@/sse/utils/logger"; -import { toJsonErrorPayload } from "@/shared/utils/upstreamError"; import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy"; -import { v1ImageGenerationSchema } from "@/shared/validation/schemas"; -import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { isAllRateLimitedCredentials, rateLimitedProviderResponse, } from "@/app/api/v1/_shared/rateLimit"; -import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta"; -import { calculateModalCost } from "@/lib/usage/costCalculator"; -import { generateRequestId } from "@/shared/utils/requestId"; +import { + failedMediaGenerationResponse, + mediaGenerationModelListResponse, + mediaGenerationOptionsResponse, + promptRequiredResponse, + readMediaGenerationBody, + successfulMediaGenerationResponse, +} from "@/app/api/v1/_shared/mediaGenerationRoute"; /** * Handle CORS preflight */ export async function OPTIONS() { - return new Response(null, { - headers: { - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "*", - }, - }); + return mediaGenerationOptionsResponse(); } /** * GET /v1/music/generations — list available music models */ export async function GET() { - const models = getAllMusicModels(); - return new Response( - JSON.stringify({ - object: "list", - data: models.map((m) => ({ - id: m.id, - object: "model", - created: Math.floor(Date.now() / 1000), - owned_by: m.provider, - type: "music", - })), - }), - { - headers: { "Content-Type": "application/json" }, - } - ); + return mediaGenerationModelListResponse(getAllMusicModels(), "music"); } /** * POST /v1/music/generations — generate music */ async function postHandler(request, context) { - let rawBody; - try { - rawBody = await request.json(); - } catch { - log.warn("MUSIC", "Invalid JSON body"); - return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body"); + const parsed = await readMediaGenerationBody(request, log, "MUSIC"); + if (!parsed.ok) { + return parsed.response; } - - const validation = validateBody(v1ImageGenerationSchema, rawBody); - if (isValidationFailure(validation)) { - return errorResponse(HTTP_STATUS.BAD_REQUEST, validation.error.message); - } - const body = validation.data; + const body = parsed.body; const startTime = Date.now(); - if (typeof body.prompt !== "string" || body.prompt.trim().length === 0) { - return errorResponse(HTTP_STATUS.BAD_REQUEST, "Prompt is required"); - } + const promptError = promptRequiredResponse(body); + if (promptError) return promptError; // Enforce API key policies (model restrictions + budget limits) const policy = await enforceApiKeyPolicy(request, body.model); @@ -118,28 +86,17 @@ async function postHandler(request, context) { if (result.success) { await clearRecoveredProviderState(credentials); - // Music is billed per-second like audio. - const seconds = Number(body.duration) || 0; - const costUsd = await calculateModalCost("audio", provider, body.model, { seconds }); - const headers = new Headers({ "Content-Type": "application/json" }); - attachOmniRouteMetaHeaders(headers, { + return successfulMediaGenerationResponse({ + result, + billingMode: "audio", provider, model: body.model, - costUsd, - latencyMs: Date.now() - startTime, - requestId: generateRequestId(), - }); - return new Response(JSON.stringify((result as { data: unknown }).data), { - status: 200, - headers, + startTime, + duration: body.duration, }); } - const errorPayload = toJsonErrorPayload((result as any).error, "Music generation provider error"); - return new Response(JSON.stringify(errorPayload), { - status: (result as any).status, - headers: { "Content-Type": "application/json" }, - }); + return failedMediaGenerationResponse(result, "Music generation provider error"); } export const POST = withInjectionGuard(postHandler); diff --git a/src/app/api/v1/relay/chat/completions/bifrost/route.ts b/src/app/api/v1/relay/chat/completions/bifrost/route.ts index 936cbc7a64..6d1c29b0a4 100644 --- a/src/app/api/v1/relay/chat/completions/bifrost/route.ts +++ b/src/app/api/v1/relay/chat/completions/bifrost/route.ts @@ -30,11 +30,7 @@ import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors"; import { createInjectionGuard } from "@/middleware/promptInjectionGuard"; -import { - getRelayTokenByHash, - checkRateLimit, - recordRelayUsage, -} from "@/lib/db/relayProxies"; +import { getRelayTokenByHash, checkRateLimit, recordRelayUsage } from "@/lib/db/relayProxies"; import { buildErrorBody } from "@omniroute/open-sse/utils/error"; import { z } from "zod"; import { @@ -44,6 +40,7 @@ import { hashToken, sanitizeForensicHeader, } from "../relaySecurity"; +import { finalizeReadableStream } from "../streamFinalizer"; // Minimal request-shape validation (Rule #7). `.passthrough()` keeps every other // OpenAI chat-completion field intact (temperature, tools, response_format, …) — @@ -76,44 +73,6 @@ export async function OPTIONS() { return handleCorsOptions(); } -function finalizeReadableStream( - body: ReadableStream, - onFinalize: (error?: unknown) => void -): ReadableStream { - const reader = body.getReader(); - let finalized = false; - - const finalizeOnce = (error?: unknown) => { - if (finalized) return; - finalized = true; - onFinalize(error); - }; - - return new ReadableStream({ - async pull(controller) { - try { - const { done, value } = await reader.read(); - if (done) { - finalizeOnce(); - controller.close(); - return; - } - controller.enqueue(value); - } catch (error) { - finalizeOnce(error); - controller.error(error); - } - }, - async cancel(reason) { - try { - await reader.cancel(reason); - } finally { - finalizeOnce(reason); - } - }, - }); -} - export async function POST(request: Request) { const startTime = Date.now(); const clientIp = getClientIp(request); @@ -249,7 +208,9 @@ export async function POST(request: Request) { const parsed = BifrostRequestSchema.safeParse(rawBody); if (!parsed.success) { return new Response( - JSON.stringify(buildErrorBody(400, parsed.error.issues[0]?.message || "Invalid request body")), + JSON.stringify( + buildErrorBody(400, parsed.error.issues[0]?.message || "Invalid request body") + ), { status: 400, headers: JSON_CORS_HEADERS } ); } diff --git a/src/app/api/v1/relay/chat/completions/bifrostCooldown.ts b/src/app/api/v1/relay/chat/completions/bifrostCooldown.ts new file mode 100644 index 0000000000..7ef5d4caf5 --- /dev/null +++ b/src/app/api/v1/relay/chat/completions/bifrostCooldown.ts @@ -0,0 +1,54 @@ +interface CooldownEntry { + until: number; + reason: string; +} + +const cooldowns = new Map(); + +export interface ActiveBifrostCooldown { + remainingMs: number; + reason: string; +} + +export function getBifrostFailureCooldownMs(env: NodeJS.ProcessEnv = process.env): number { + const parsed = Number.parseInt(env.OMNIROUTE_BIFROST_FAILURE_COOLDOWN_MS || "", 10); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : 5000; +} + +export function getActiveBifrostCooldown( + baseUrl: string, + now = Date.now() +): ActiveBifrostCooldown | null { + const entry = cooldowns.get(baseUrl); + if (!entry) return null; + if (entry.until <= now) { + cooldowns.delete(baseUrl); + return null; + } + + return { + remainingMs: entry.until - now, + reason: entry.reason, + }; +} + +export function recordBifrostFailure( + baseUrl: string, + reason: string, + now = Date.now(), + cooldownMs = getBifrostFailureCooldownMs() +): void { + if (cooldownMs <= 0) { + cooldowns.delete(baseUrl); + return; + } + cooldowns.set(baseUrl, { until: now + cooldownMs, reason }); +} + +export function clearBifrostFailure(baseUrl: string): void { + cooldowns.delete(baseUrl); +} + +export function resetBifrostCooldowns(): void { + cooldowns.clear(); +} diff --git a/src/app/api/v1/relay/chat/completions/route.ts b/src/app/api/v1/relay/chat/completions/route.ts index 32bae3ef08..d8d7afd693 100644 --- a/src/app/api/v1/relay/chat/completions/route.ts +++ b/src/app/api/v1/relay/chat/completions/route.ts @@ -20,10 +20,17 @@ import { } from "./relaySecurity"; import { getBifrostRoutingConfig, + getRoutingFallbackHeader, resolveRelayRoutingBackend, shouldTryBifrost, type BifrostRoutingConfig, } from "./routingBackend"; +import { finalizeReadableStream } from "./streamFinalizer"; +import { + clearBifrostFailure, + getActiveBifrostCooldown, + recordBifrostFailure, +} from "./bifrostCooldown"; import type { RelayToken } from "@/lib/db/relayProxies"; const JSON_CORS_HEADERS = { ...CORS_HEADERS, "Content-Type": "application/json" } as const; @@ -51,44 +58,6 @@ function recordUsage( }); } -function finalizeReadableStream( - body: ReadableStream, - onFinalize: (error?: unknown) => void -): ReadableStream { - const reader = body.getReader(); - let finalized = false; - - const finalizeOnce = (error?: unknown) => { - if (finalized) return; - finalized = true; - onFinalize(error); - }; - - return new ReadableStream({ - async pull(controller) { - try { - const { done, value } = await reader.read(); - if (done) { - finalizeOnce(); - controller.close(); - return; - } - controller.enqueue(value); - } catch (error) { - finalizeOnce(error); - controller.error(error); - } - }, - async cancel(reason) { - try { - await reader.cancel(reason); - } finally { - finalizeOnce(reason); - } - }, - }); -} - async function forwardToBifrost( request: Request, body: unknown, @@ -320,32 +289,39 @@ export async function POST(request: Request) { const backend = resolveRelayRoutingBackend(); const bifrostConfig = getBifrostRoutingConfig(); + let bifrostFallbackReason: string | null = null; if (shouldTryBifrost(backend, bifrostConfig)) { - try { - return await forwardToBifrost( - request, - parsedBody, - token, - bifrostConfig, - startTime, - clientIp, - userAgent - ); - } catch (error) { - if (backend === "bifrost") { - recordUsage(token.id, request, startTime, clientIp, userAgent, "error", 502); - return new Response( - JSON.stringify( - buildErrorBody(502, error instanceof Error ? error.message : String(error)) - ), - { + const cooldown = + backend === "auto" ? getActiveBifrostCooldown(bifrostConfig.baseUrl) : null; + if (cooldown) { + bifrostFallbackReason = `bifrost-cooldown; remaining=${cooldown.remainingMs}`; + } else { + try { + const bifrostResponse = await forwardToBifrost( + request, + parsedBody, + token, + bifrostConfig, + startTime, + clientIp, + userAgent + ); + clearBifrostFailure(bifrostConfig.baseUrl); + return bifrostResponse; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (backend === "bifrost") { + recordUsage(token.id, request, startTime, clientIp, userAgent, "error", 502); + return new Response(JSON.stringify(buildErrorBody(502, message)), { status: 502, headers: { ...JSON_CORS_HEADERS, "X-Bifrost-Fallback": "/api/v1/relay/chat/completions", }, - } - ); + }); + } + recordBifrostFailure(bifrostConfig.baseUrl, message); + bifrostFallbackReason = "bifrost-error"; } } } @@ -372,8 +348,11 @@ export async function POST(request: Request) { const newHeaders = new Headers(response.headers); newHeaders.set("X-Relay-Token", token.tokenPrefix + "..."); newHeaders.set("X-Routing-Backend", "ts"); - if (backend === "auto" && bifrostConfig?.enabled) { - newHeaders.set("X-Routing-Fallback", "bifrost"); + const routingFallback = getRoutingFallbackHeader(backend, bifrostConfig); + if (routingFallback) { + // #5526 helper gates emission (auto + enabled); #5519 dynamic cooldown/error + // reason wins as the value when set, else falls back to the static "bifrost". + newHeaders.set("X-Routing-Fallback", bifrostFallbackReason ?? routingFallback); } return new Response(response.body, { diff --git a/src/app/api/v1/relay/chat/completions/routingBackend.ts b/src/app/api/v1/relay/chat/completions/routingBackend.ts index 36d4a6eae0..8604065c04 100644 --- a/src/app/api/v1/relay/chat/completions/routingBackend.ts +++ b/src/app/api/v1/relay/chat/completions/routingBackend.ts @@ -10,7 +10,9 @@ export interface BifrostRoutingConfig { enabled: boolean; } -export function getBifrostRoutingConfig(env: NodeJS.ProcessEnv = process.env): BifrostRoutingConfig | null { +export function getBifrostRoutingConfig( + env: NodeJS.ProcessEnv = process.env +): BifrostRoutingConfig | null { const baseUrl = env.BIFROST_BASE_URL?.replace(/\/$/, ""); if (!baseUrl) return null; const timeoutMs = Number.parseInt(env.BIFROST_TIMEOUT_MS || "", 10); @@ -24,7 +26,9 @@ export function getBifrostRoutingConfig(env: NodeJS.ProcessEnv = process.env): B }; } -export function resolveRelayRoutingBackend(env: NodeJS.ProcessEnv = process.env): RelayRoutingBackend { +export function resolveRelayRoutingBackend( + env: NodeJS.ProcessEnv = process.env +): RelayRoutingBackend { const configured = env.OMNIROUTE_RELAY_BACKEND || env.RELAY_ROUTING_BACKEND; if (configured && VALID_BACKENDS.has(configured as RelayRoutingBackend)) { return configured as RelayRoutingBackend; @@ -39,3 +43,10 @@ export function shouldTryBifrost( ): config is BifrostRoutingConfig { return Boolean(config?.enabled && backend !== "ts"); } + +export function getRoutingFallbackHeader( + backend: RelayRoutingBackend, + config: BifrostRoutingConfig | null +): "bifrost" | undefined { + return backend === "auto" && config?.enabled ? "bifrost" : undefined; +} diff --git a/src/app/api/v1/relay/chat/completions/streamFinalizer.ts b/src/app/api/v1/relay/chat/completions/streamFinalizer.ts new file mode 100644 index 0000000000..4eb6aeada0 --- /dev/null +++ b/src/app/api/v1/relay/chat/completions/streamFinalizer.ts @@ -0,0 +1,37 @@ +export function finalizeReadableStream( + body: ReadableStream, + onFinalize: (error?: unknown) => void +): ReadableStream { + const reader = body.getReader(); + let finalized = false; + + const finalizeOnce = (error?: unknown) => { + if (finalized) return; + finalized = true; + onFinalize(error); + }; + + return new ReadableStream({ + async pull(controller) { + try { + const { done, value } = await reader.read(); + if (done) { + finalizeOnce(); + controller.close(); + return; + } + controller.enqueue(value); + } catch (error) { + finalizeOnce(error); + controller.error(error); + } + }, + async cancel(reason) { + try { + await reader.cancel(reason); + } finally { + finalizeOnce(reason); + } + }, + }); +} diff --git a/src/app/api/v1/videos/generations/route.ts b/src/app/api/v1/videos/generations/route.ts index 8f0153f85a..0f730fa3b3 100644 --- a/src/app/api/v1/videos/generations/route.ts +++ b/src/app/api/v1/videos/generations/route.ts @@ -1,12 +1,7 @@ import { handleVideoGeneration } from "@omniroute/open-sse/handlers/videoGeneration.ts"; import { resolveVideoCredentialProvider } from "@omniroute/open-sse/handlers/videoGeneration/googleFlow.ts"; import { withInjectionGuard } from "@/middleware/promptInjectionGuard"; -import { - getProviderCredentials, - clearRecoveredProviderState, - extractApiKey, - isValidApiKey, -} from "@/sse/services/auth"; +import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth"; import { parseVideoModel, getAllVideoModels, @@ -15,74 +10,47 @@ import { import { errorResponse } from "@omniroute/open-sse/utils/error.ts"; import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; import * as log from "@/sse/utils/logger"; -import { toJsonErrorPayload } from "@/shared/utils/upstreamError"; import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy"; -import { v1ImageGenerationSchema } from "@/shared/validation/schemas"; -import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { isAllRateLimitedCredentials, rateLimitedProviderResponse, } from "@/app/api/v1/_shared/rateLimit"; -import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta"; -import { calculateModalCost } from "@/lib/usage/costCalculator"; -import { generateRequestId } from "@/shared/utils/requestId"; +import { + failedMediaGenerationResponse, + mediaGenerationModelListResponse, + mediaGenerationOptionsResponse, + promptRequiredResponse, + readMediaGenerationBody, + successfulMediaGenerationResponse, +} from "@/app/api/v1/_shared/mediaGenerationRoute"; /** * Handle CORS preflight */ export async function OPTIONS() { - return new Response(null, { - headers: { - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "*", - }, - }); + return mediaGenerationOptionsResponse(); } /** * GET /v1/videos/generations — list available video models */ export async function GET() { - const models = getAllVideoModels(); - return new Response( - JSON.stringify({ - object: "list", - data: models.map((m) => ({ - id: m.id, - object: "model", - created: Math.floor(Date.now() / 1000), - owned_by: m.provider, - type: "video", - })), - }), - { - headers: { "Content-Type": "application/json" }, - } - ); + return mediaGenerationModelListResponse(getAllVideoModels(), "video"); } /** * POST /v1/videos/generations — generate videos */ async function postHandler(request, context) { - let rawBody; - try { - rawBody = await request.json(); - } catch { - log.warn("VIDEO", "Invalid JSON body"); - return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body"); + const parsed = await readMediaGenerationBody(request, log, "VIDEO"); + if (!parsed.ok) { + return parsed.response; } - - const validation = validateBody(v1ImageGenerationSchema, rawBody); - if (isValidationFailure(validation)) { - return errorResponse(HTTP_STATUS.BAD_REQUEST, validation.error.message); - } - const body = validation.data; + const body = parsed.body; const startTime = Date.now(); - if (typeof body.prompt !== "string" || body.prompt.trim().length === 0) { - return errorResponse(HTTP_STATUS.BAD_REQUEST, "Prompt is required"); - } + const promptError = promptRequiredResponse(body); + if (promptError) return promptError; // Enforce API key policies (model restrictions + budget limits) const policy = await enforceApiKeyPolicy(request, body.model); @@ -121,27 +89,17 @@ async function postHandler(request, context) { if (result.success) { await clearRecoveredProviderState(credentials); - const seconds = Number(body.duration) || 0; - const costUsd = await calculateModalCost("video", provider, body.model, { seconds }); - const headers = new Headers({ "Content-Type": "application/json" }); - attachOmniRouteMetaHeaders(headers, { + return successfulMediaGenerationResponse({ + result, + billingMode: "video", provider, model: body.model, - costUsd, - latencyMs: Date.now() - startTime, - requestId: generateRequestId(), - }); - return new Response(JSON.stringify((result as { data: unknown }).data), { - status: 200, - headers, + startTime, + duration: body.duration, }); } - const errorPayload = toJsonErrorPayload((result as any).error, "Video generation provider error"); - return new Response(JSON.stringify(errorPayload), { - status: (result as any).status, - headers: { "Content-Type": "application/json" }, - }); + return failedMediaGenerationResponse(result, "Video generation provider error"); } export const POST = withInjectionGuard(postHandler); diff --git a/src/app/api/v1/vscode/[token]/familyFirstModelIds.ts b/src/app/api/v1/vscode/[token]/familyFirstModelIds.ts index c5a2484761..c1ef838b6b 100644 --- a/src/app/api/v1/vscode/[token]/familyFirstModelIds.ts +++ b/src/app/api/v1/vscode/[token]/familyFirstModelIds.ts @@ -1,85 +1,5 @@ -const FAMILY_FIRST_MODEL_PATTERN = /^((?:gpt-[a-z0-9._-]+|claude[a-z0-9._-]*))(?:__provider_([a-z0-9-]+))(?:__tier_(priority|flex))?$/i; -const TIER_SUFFIX_PATTERN = /(__tier_(?:priority|flex))$/i; - -function normalizeFamily(value: string | null | undefined) { - return typeof value === "string" ? value.trim() : ""; -} - -function splitActualModelId(modelId: string) { - const trimmedModelId = modelId.trim(); - const slashIndex = trimmedModelId.indexOf("/"); - if (slashIndex <= 0 || slashIndex === trimmedModelId.length - 1) { - return null; - } - - return { - providerPrefix: trimmedModelId.slice(0, slashIndex), - providerModelId: trimmedModelId.slice(slashIndex + 1), - }; -} - -function extractTierSuffix(modelId: string) { - const match = modelId.match(TIER_SUFFIX_PATTERN); - return match?.[1] || ""; -} - -function stripTierSuffix(modelId: string) { - return modelId.replace(TIER_SUFFIX_PATTERN, ""); -} - -function isFamilyFirstEligibleFamily(family: string) { - const normalized = family.toLowerCase(); - return normalized.startsWith("gpt-") || normalized.startsWith("claude"); -} - -export function getFamilyFirstPublishedModelId(actualModelId: string, family: string | null | undefined) { - const normalizedFamily = normalizeFamily(family); - if (!normalizedFamily || !isFamilyFirstEligibleFamily(normalizedFamily)) { - return actualModelId; - } - - const parts = splitActualModelId(actualModelId); - if (!parts) { - return actualModelId; - } - - const tierSuffix = extractTierSuffix(parts.providerModelId); - const providerModelBase = stripTierSuffix(parts.providerModelId); - if (providerModelBase !== normalizedFamily) { - return actualModelId; - } - - return `${normalizedFamily}__provider_${parts.providerPrefix}${tierSuffix}`; -} - -export function resolveFamilyFirstPublishedModelId(modelId: string | null | undefined) { - const trimmedModelId = typeof modelId === "string" ? modelId.trim() : ""; - if (!trimmedModelId) { - return trimmedModelId; - } - - const match = trimmedModelId.match(FAMILY_FIRST_MODEL_PATTERN); - if (!match) { - return trimmedModelId; - } - - const [, family, providerPrefix, serviceTier] = match; - const tierSuffix = serviceTier ? `__tier_${serviceTier.toLowerCase()}` : ""; - return `${providerPrefix}/${family}${tierSuffix}`; -} - -export function getFamilyFirstModelCandidates(actualModelId: string, family: string | null | undefined) { - const normalizedFamily = normalizeFamily(family); - const candidates = new Set([actualModelId]); - const publishedModelId = getFamilyFirstPublishedModelId(actualModelId, normalizedFamily); - if (publishedModelId !== actualModelId) { - candidates.add(publishedModelId); - } - - if (normalizedFamily && isFamilyFirstEligibleFamily(normalizedFamily)) { - const tierSuffix = extractTierSuffix(actualModelId); - candidates.add(`${normalizedFamily}${tierSuffix}`); - } - - return [...candidates]; -} \ No newline at end of file +export { + getFamilyFirstModelCandidates, + getFamilyFirstPublishedModelId, + resolveFamilyFirstPublishedModelId, +} from "@/lib/vscode/familyFirstModelIds"; diff --git a/src/app/api/v1/vscode/[token]/modelPresentation.ts b/src/app/api/v1/vscode/[token]/modelPresentation.ts index 510467d5dc..be09ed3bf6 100644 --- a/src/app/api/v1/vscode/[token]/modelPresentation.ts +++ b/src/app/api/v1/vscode/[token]/modelPresentation.ts @@ -1,177 +1,5 @@ -import { parseModel } from "@omniroute/open-sse/services/model"; -import { - getCanonicalModelMetadata, - type CanonicalModelMetadata, -} from "@/lib/modelMetadataRegistry"; -import { - getVscodeServiceTierVariantSuffix, - parseVscodeServiceTierVariantModelId, - supportsVscodeServiceTierVariants, -} from "@/app/api/v1/vscode/[token]/serviceTierVariants"; -import { getReasoningVariantBaseModelId } from "@/app/api/v1/vscode/[token]/reasoningMetadata"; -import { resolveFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/[token]/familyFirstModelIds"; -type VscodeCatalogModel = { - id?: string; - name?: string; - root?: string; - owned_by?: string; -}; - -const PROVIDER_NAME_OVERRIDES: Record = { - codex: "Codex", - cx: "Codex", - github: "GitHub", - gh: "GitHub", - gemini: "Gemini", -}; - -function tokenize(value: string) { - return value - .toLowerCase() - .split(/[^a-z0-9]+/i) - .map((part) => part.trim()) - .filter((part) => part.length >= 4); -} - -function getProviderPrefix(metadata: CanonicalModelMetadata | null) { - const providerKey = metadata?.providerAlias || metadata?.provider || ""; - if (providerKey && PROVIDER_NAME_OVERRIDES[providerKey]) { - return PROVIDER_NAME_OVERRIDES[providerKey]; - } - - const providerLabel = metadata?.providerLabel?.trim() || null; - if (!providerLabel) { - return null; - } - if (/codex/i.test(providerLabel)) { - return "Codex"; - } - if (/github/i.test(providerLabel)) { - return "GitHub"; - } - if (/gemini/i.test(providerLabel)) { - return "Gemini"; - } - - return providerLabel; -} - -function normalizeDisplayNameBranding(displayName: string) { - return displayName - .replace(/^OpenAI\s+Codex\b/i, "Codex") - .replace(/^GitHub\s+Copilot\b/i, "GitHub") - .trim(); -} - -function stripLeadingProviderPrefix(displayName: string, providerPrefix: string | null) { - if (!providerPrefix) { - return displayName; - } - - const escapedProviderPrefix = providerPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - return displayName.replace(new RegExp(`^${escapedProviderPrefix}\\s+`, "i"), "").trim(); -} - -function looksLikeTechnicalModelName(value: string) { - return /\/|__provider_|__tier_|^[a-z0-9-]+\/[a-z0-9._-]+$/i.test(value); -} - -function humanizeModelIdentifier(modelId: string) { - const identifier = (modelId.split("/").pop() || modelId).trim(); - if (!identifier) { - return identifier; - } - - return identifier - .split(/[-_]+/) - .filter(Boolean) - .map((part) => { - if (/^gpt$/i.test(part)) return "GPT"; - if (/^[0-9]+(?:\.[0-9]+)*$/.test(part)) return part; - if (/^[a-z][0-9]$/i.test(part)) return part.toUpperCase(); - return part.charAt(0).toUpperCase() + part.slice(1); - }) - .join(" "); -} - -function resolveFriendlyBaseDisplayName( - rawModelId: string, - metadata: CanonicalModelMetadata | null, - fallbackValue: string -) { - const normalizedFallback = normalizeDisplayNameBranding(fallbackValue.trim()); - if (normalizedFallback && !looksLikeTechnicalModelName(normalizedFallback)) { - return normalizedFallback; - } - - const normalizedModelId = resolveFamilyFirstPublishedModelId(rawModelId); - const parsedTierModel = parseVscodeServiceTierVariantModelId(normalizedModelId); - const canonicalBaseModelId = getReasoningVariantBaseModelId(parsedTierModel.baseModelId); - const parsed = parseModel(canonicalBaseModelId, ""); - const providerModelId = - parsed.model || - (canonicalBaseModelId.includes("/") - ? canonicalBaseModelId.split("/").slice(1).join("/") - : canonicalBaseModelId) || - fallbackValue; - - return humanizeModelIdentifier(providerModelId); -} - -function prefixDisplayName(displayName: string, providerPrefix: string | null) { - const normalizedProviderPrefix = providerPrefix?.trim() || null; - const normalizedDisplayName = normalizeDisplayNameBranding(displayName); - - if (!normalizedProviderPrefix) return normalizedDisplayName; - - const providerTokens = tokenize(normalizedProviderPrefix); - if (providerTokens.length === 0) return normalizedDisplayName; - - const displayNameLower = normalizedDisplayName.toLowerCase(); - if (providerTokens.some((token) => displayNameLower.includes(token))) { - return normalizedDisplayName; - } - - return `${normalizedProviderPrefix} ${stripLeadingProviderPrefix(normalizedDisplayName, normalizedProviderPrefix)}`.trim(); -} - -export function resolveVscodeModelMetadata(model: VscodeCatalogModel) { - const rawModelId = model.id || model.root || model.name || ""; - const normalizedModelId = resolveFamilyFirstPublishedModelId(rawModelId); - const parsedTierModel = parseVscodeServiceTierVariantModelId(normalizedModelId); - const canonicalBaseModelId = getReasoningVariantBaseModelId(parsedTierModel.baseModelId); - const parsed = parseModel(canonicalBaseModelId, ""); - const provider = parsed.provider || model.owned_by || undefined; - const providerModel = - parsed.model || - (canonicalBaseModelId.includes("/") - ? canonicalBaseModelId.split("/").slice(1).join("/") - : canonicalBaseModelId) || - model.root || - model.id || - model.name || - undefined; - - return providerModel && provider - ? getCanonicalModelMetadata({ provider, model: providerModel }) - : providerModel - ? getCanonicalModelMetadata({ model: providerModel }) - : null; -} - -export function getVscodeModelDisplayName(model: VscodeCatalogModel) { - const rawModelId = model.id || model.root || model.name || ""; - const { serviceTier } = parseVscodeServiceTierVariantModelId(rawModelId); - const metadata = resolveVscodeModelMetadata(model); - const displayName = metadata?.displayName || model.name || model.id || model.root || "unknown"; - const prefixedDisplayName = prefixDisplayName(displayName, getProviderPrefix(metadata)); - const shouldShowTierSuffix = Boolean(serviceTier) || supportsVscodeServiceTierVariants(model); - return shouldShowTierSuffix - ? `${prefixedDisplayName} (${getVscodeServiceTierVariantSuffix(serviceTier)})` - : prefixedDisplayName; -} - -export function getVscodeModelGroupingKey(model: VscodeCatalogModel) { - const metadata = resolveVscodeModelMetadata(model); - return metadata?.qualifiedId || metadata?.model || model.id || model.name || model.root || ""; -} +export { + getVscodeModelDisplayName, + getVscodeModelGroupingKey, + resolveVscodeModelMetadata, +} from "@/lib/vscode/modelPresentation"; diff --git a/src/app/api/v1/vscode/[token]/reasoningMetadata.ts b/src/app/api/v1/vscode/[token]/reasoningMetadata.ts index e7996a8883..3153457309 100644 --- a/src/app/api/v1/vscode/[token]/reasoningMetadata.ts +++ b/src/app/api/v1/vscode/[token]/reasoningMetadata.ts @@ -1,175 +1,13 @@ -import { parseModel } from "@omniroute/open-sse/services/model"; -import { supportsXHighEffort } from "@omniroute/open-sse/config/providerModels"; -import { stripVscodeServiceTierVariantModelId } from "@/app/api/v1/vscode/[token]/serviceTierVariants"; - -export type VscodeCatalogModel = { - id?: string; - name?: string; - root?: string; - owned_by?: string; - capabilities?: Record; - supportsReasoningEffort?: string[]; - supportedReasoningEfforts?: string[]; - supports_reasoning_effort?: string[]; - defaultReasoningEffort?: string; - default_reasoning_effort?: string; -}; - -const EFFORT_SUFFIX_PATTERN = /-(xhigh|high|medium|low|none)$/i; -const DEFAULT_REASONING_EFFORT = "none"; -const KNOWN_REASONING_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh"]); - -export type VscodeModelConfigSchema = { - type: "object"; - properties: { - reasoningEffort: { - type: "string"; - title: string; - description: string; - default: string; - enum: string[]; - enumLabels: string[]; - enumDescriptions: string[]; - }; - }; -}; - -export function getCatalogModelName(model: VscodeCatalogModel) { - return stripVscodeServiceTierVariantModelId(model.id || model.name || model.root || ""); -} - -function normalizeReasoningEffortValue(value: string) { - const normalized = value.trim().toLowerCase().replace(/[_\s-]+/g, ""); - if (normalized === "xhigh") return "xhigh"; - if (KNOWN_REASONING_EFFORTS.has(normalized)) return normalized; - return undefined; -} - -function getNativeReasoningEffortValues(model: VscodeCatalogModel) { - const candidates = [ - model.supportsReasoningEffort, - model.supportedReasoningEfforts, - model.supports_reasoning_effort, - ]; - - for (const candidate of candidates) { - if (!Array.isArray(candidate) || candidate.length === 0) { - continue; - } - - const normalized = Array.from( - new Set(candidate.map(value => typeof value === "string" ? normalizeReasoningEffortValue(value) : undefined).filter(Boolean)) - ) as string[]; - - if (normalized.length > 0) { - return normalized; - } - } - - return undefined; -} - -export function isReasoningCapableModel(model: VscodeCatalogModel) { - return ( - model.capabilities?.reasoning === true || - model.capabilities?.thinking === true || - (getNativeReasoningEffortValues(model)?.length || 0) > 0 - ); -} - -export function getReasoningEffortValues(model: VscodeCatalogModel) { - const nativeReasoningEffortValues = getNativeReasoningEffortValues(model); - if (nativeReasoningEffortValues && nativeReasoningEffortValues.length > 0) { - return nativeReasoningEffortValues; - } - - if (!isReasoningCapableModel(model)) return undefined; - - const modelId = getCatalogModelName(model); - const parsed = parseModel(modelId, ""); - const providerId = parsed.provider || model.owned_by || ""; - const providerModelId = parsed.model || model.root || modelId.split("/").pop() || modelId; - const values = ["none", "low", "medium", "high"]; - - if (providerId && providerModelId && supportsXHighEffort(providerId, providerModelId)) { - values.push("xhigh"); - } - - return values; -} - -export function formatReasoningEffortLabel(level: string) { - if (level === "xhigh") return "XHigh"; - return level.charAt(0).toUpperCase() + level.slice(1); -} - -function describeReasoningEffort(level: string) { - switch (level) { - case "none": - return "Disables extra reasoning effort."; - case "low": - return "Uses a light amount of reasoning."; - case "medium": - return "Uses a balanced amount of reasoning."; - case "high": - return "Uses an extended amount of reasoning."; - case "xhigh": - return "Uses the maximum available reasoning effort."; - default: - return `Uses ${formatReasoningEffortLabel(level)} reasoning effort.`; - } -} - -export function buildSupportedReasoningEfforts( - supportedValues: string[] -): string[] { - return [...supportedValues]; -} - -export function inferSelectedReasoningEffort( - model: VscodeCatalogModel, - supportedValues?: string[] -) { - const modelId = getCatalogModelName(model); - const match = modelId.match(EFFORT_SUFFIX_PATTERN); - if (!match) return undefined; - - const selected = match[1]?.toLowerCase(); - if (!selected) return undefined; - if (Array.isArray(supportedValues) && supportedValues.length > 0 && !supportedValues.includes(selected)) { - return undefined; - } - - return selected; -} - -export function getReasoningVariantBaseModelId(modelId: string) { - return modelId.replace(EFFORT_SUFFIX_PATTERN, ""); -} - -export function getDefaultReasoningEffort( - model: VscodeCatalogModel, - supportedValues?: string[] -) { - return inferSelectedReasoningEffort(model, supportedValues) || DEFAULT_REASONING_EFFORT; -} - -export function buildReasoningConfigSchema( - supportedValues: string[], - defaultReasoningEffort: string -): VscodeModelConfigSchema { - return { - type: "object", - properties: { - reasoningEffort: { - type: "string", - title: "Reasoning effort", - description: "Controls how much reasoning effort the model uses.", - default: defaultReasoningEffort, - enum: supportedValues, - enumLabels: supportedValues.map(formatReasoningEffortLabel), - enumDescriptions: supportedValues.map(describeReasoningEffort), - }, - }, - }; -} +export { + buildReasoningConfigSchema, + buildSupportedReasoningEfforts, + formatReasoningEffortLabel, + getCatalogModelName, + getDefaultReasoningEffort, + getReasoningEffortValues, + getReasoningVariantBaseModelId, + inferSelectedReasoningEffort, + isReasoningCapableModel, + type VscodeCatalogModel, + type VscodeModelConfigSchema, +} from "@/lib/vscode/reasoningMetadata"; diff --git a/src/app/api/v1/vscode/[token]/serviceTierVariants.ts b/src/app/api/v1/vscode/[token]/serviceTierVariants.ts index 8e69f224c3..08bfb6ef3b 100644 --- a/src/app/api/v1/vscode/[token]/serviceTierVariants.ts +++ b/src/app/api/v1/vscode/[token]/serviceTierVariants.ts @@ -1,190 +1,12 @@ -import { CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS } from "@/lib/providers/codexFastTier"; -import { normalizeServiceTierId, type ServiceTierId } from "@/shared/utils/serviceTierLabels"; -import { resolveFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/[token]/familyFirstModelIds"; - -const SERVICE_TIER_VARIANT_PATTERN = /__tier_(priority|flex)$/i; -const SUPPORTED_VSCODE_SERVICE_TIERS: readonly ServiceTierId[] = ["priority", "flex"]; - -export type VscodeServiceTierModelLike = { - id?: string; - name?: string; - root?: string; - owned_by?: string; -}; - -export function parseVscodeServiceTierVariantModelId(modelId: string | null | undefined): { - baseModelId: string; - serviceTier?: ServiceTierId; -} { - const rawModelId = typeof modelId === "string" ? modelId.trim() : ""; - if (!rawModelId) { - return { baseModelId: "" }; - } - - const match = rawModelId.match(SERVICE_TIER_VARIANT_PATTERN); - if (!match) { - return { baseModelId: rawModelId }; - } - - const baseModelId = rawModelId.replace(SERVICE_TIER_VARIANT_PATTERN, ""); - const serviceTier = normalizeServiceTierId(match[1]); - return serviceTier === "standard" ? { baseModelId } : { baseModelId, serviceTier }; -} - -export function stripVscodeServiceTierVariantModelId(modelId: string | null | undefined): string { - return parseVscodeServiceTierVariantModelId(modelId).baseModelId; -} - -export function isVscodeServiceTierVariantModelId(modelId: string | null | undefined): boolean { - return Boolean(parseVscodeServiceTierVariantModelId(modelId).serviceTier); -} - -export function getVscodeServiceTierVariantModelId( - baseModelId: string, - serviceTier: ServiceTierId -): string { - if (serviceTier === "standard") { - return baseModelId; - } - return `${baseModelId}__tier_${serviceTier}`; -} - -function getRawModelId(model: VscodeServiceTierModelLike): string { - return (model.id || model.name || model.root || "").trim(); -} - -function getModelProvider(model: VscodeServiceTierModelLike, baseModelId: string): string { - const owner = typeof model.owned_by === "string" ? model.owned_by.trim().toLowerCase() : ""; - if (owner) { - return owner; - } - const prefix = baseModelId.split("/")[0]?.trim().toLowerCase() || ""; - return prefix; -} - -function supportsCodexServiceTierModel(baseModelId: string): boolean { - const normalizedModel = (baseModelId.split("/").pop() || baseModelId).trim().toLowerCase(); - if (!normalizedModel) { - return false; - } - - return CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS.some((candidate) => { - const normalizedCandidate = candidate.trim().toLowerCase(); - return normalizedModel === normalizedCandidate || normalizedModel.startsWith(normalizedCandidate); - }); -} - -export function supportsVscodeServiceTierVariants(model: VscodeServiceTierModelLike): boolean { - const rawModelId = getRawModelId(model); - if (!rawModelId) { - return false; - } - - const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId); - const provider = getModelProvider(model, baseModelId); - if (provider !== "codex" && provider !== "cx") { - return false; - } - - return supportsCodexServiceTierModel(baseModelId); -} - -function cloneModelIdentifiers( - model: T, - modelId: string -): T { - return { - ...model, - ...(model.id ? { id: modelId } : {}), - ...(model.name ? { name: modelId } : {}), - ...(model.root ? { root: modelId } : {}), - }; -} - -export function expandVscodeServiceTierModels(models: T[]): T[] { - const expanded: T[] = []; - - for (const model of models) { - const rawModelId = getRawModelId(model); - if (!rawModelId) { - expanded.push(model); - continue; - } - - const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId); - const baseModel = rawModelId === baseModelId ? model : cloneModelIdentifiers(model, baseModelId); - expanded.push(baseModel as T); - - if (!supportsVscodeServiceTierVariants(model)) { - continue; - } - - for (const serviceTier of SUPPORTED_VSCODE_SERVICE_TIERS) { - expanded.push(cloneModelIdentifiers(baseModel as T, getVscodeServiceTierVariantModelId(baseModelId, serviceTier))); - } - } - - return expanded; -} - -export function getVscodeServiceTierVariantSuffix(serviceTier: ServiceTierId | undefined): string { - if (serviceTier === "priority") { - return "Fast"; - } - if (serviceTier === "flex") { - return "Flex"; - } - return "Default"; -} - -export function resolveVscodeServiceTierRequest(body: Record): Record { - const rawModelId = typeof body.model === "string" ? body.model.trim() : ""; - if (!rawModelId) { - return body; - } - - const resolvedModelId = resolveFamilyFirstPublishedModelId(rawModelId); - - const { baseModelId, serviceTier } = parseVscodeServiceTierVariantModelId(resolvedModelId); - if (!serviceTier) { - if (resolvedModelId === rawModelId) { - return body; - } - - return { - ...body, - model: resolvedModelId, - }; - } - - return { - ...body, - model: baseModelId, - ...(body.service_tier === undefined ? { service_tier: serviceTier } : {}), - }; -} - -export async function rewriteVscodeServiceTierRequest(request: Request): Promise { - if (request.method !== "POST") { - return request; - } - - const body = await request.clone().json().catch(() => null); - if (!body || typeof body !== "object" || Array.isArray(body)) { - return request; - } - - const rewrittenBody = resolveVscodeServiceTierRequest(body as Record); - if (rewrittenBody === body) { - return request; - } - - const headers = new Headers(request.headers); - headers.delete("content-length"); - - return new Request(request.url, { - method: request.method, - headers, - body: JSON.stringify(rewrittenBody), - }); -} +export { + expandVscodeServiceTierModels, + getVscodeServiceTierVariantModelId, + getVscodeServiceTierVariantSuffix, + isVscodeServiceTierVariantModelId, + parseVscodeServiceTierVariantModelId, + resolveVscodeServiceTierRequest, + rewriteVscodeServiceTierRequest, + stripVscodeServiceTierVariantModelId, + supportsVscodeServiceTierVariants, + type VscodeServiceTierModelLike, +} from "@/lib/vscode/serviceTierVariants"; diff --git a/src/app/api/v1/vscode/[token]/tokenizedRequest.ts b/src/app/api/v1/vscode/[token]/tokenizedRequest.ts index 24bb41d252..bb48fde7e7 100644 --- a/src/app/api/v1/vscode/[token]/tokenizedRequest.ts +++ b/src/app/api/v1/vscode/[token]/tokenizedRequest.ts @@ -1,56 +1 @@ -import { sanitizeVscodeRequest } from "@/app/api/v1/vscode/contextSanitizer"; - -function inferTokenFromVscodePath(request: Request) { - try { - const url = new URL(request.url, "http://localhost"); - const segments = url.pathname - .split("/") - .map((segment) => segment.trim()) - .filter(Boolean); - - const vscodeIndex = segments.indexOf("vscode"); - if (vscodeIndex === -1) return null; - - const candidate = segments[vscodeIndex + 1]; - if (!candidate || candidate === "raw" || candidate === "combos") { - const nestedCandidate = segments[vscodeIndex + 2]; - return nestedCandidate ? decodeURIComponent(nestedCandidate) : null; - } - - return decodeURIComponent(candidate); - } catch { - return null; - } -} - -export function withPathTokenApiKey(request: Request, token?: string) { - const resolvedToken = token || inferTokenFromVscodePath(request); - if (!resolvedToken) return request; - - const headers = new Headers(request.headers); - - if (!headers.has("x-api-key")) { - headers.set("x-api-key", resolvedToken); - } - - if (!headers.has("authorization")) { - headers.set("authorization", `Bearer ${resolvedToken}`); - } - - const method = request.method; - const init: RequestInit & { duplex?: "half" } = { - method, - headers, - }; - - if (method !== "GET" && method !== "HEAD") { - init.body = request.body; - init.duplex = "half"; - } - - return new Request(request.url, init); -} - -export async function withSanitizedPathTokenApiKey(request: Request, token?: string) { - return sanitizeVscodeRequest(withPathTokenApiKey(request, token)); -} +export { withPathTokenApiKey, withSanitizedPathTokenApiKey } from "@/lib/vscode/tokenizedRequest"; diff --git a/src/app/api/v1/vscode/raw/[token]/familyFirstModelIds.ts b/src/app/api/v1/vscode/raw/[token]/familyFirstModelIds.ts index c5a2484761..c1ef838b6b 100644 --- a/src/app/api/v1/vscode/raw/[token]/familyFirstModelIds.ts +++ b/src/app/api/v1/vscode/raw/[token]/familyFirstModelIds.ts @@ -1,85 +1,5 @@ -const FAMILY_FIRST_MODEL_PATTERN = /^((?:gpt-[a-z0-9._-]+|claude[a-z0-9._-]*))(?:__provider_([a-z0-9-]+))(?:__tier_(priority|flex))?$/i; -const TIER_SUFFIX_PATTERN = /(__tier_(?:priority|flex))$/i; - -function normalizeFamily(value: string | null | undefined) { - return typeof value === "string" ? value.trim() : ""; -} - -function splitActualModelId(modelId: string) { - const trimmedModelId = modelId.trim(); - const slashIndex = trimmedModelId.indexOf("/"); - if (slashIndex <= 0 || slashIndex === trimmedModelId.length - 1) { - return null; - } - - return { - providerPrefix: trimmedModelId.slice(0, slashIndex), - providerModelId: trimmedModelId.slice(slashIndex + 1), - }; -} - -function extractTierSuffix(modelId: string) { - const match = modelId.match(TIER_SUFFIX_PATTERN); - return match?.[1] || ""; -} - -function stripTierSuffix(modelId: string) { - return modelId.replace(TIER_SUFFIX_PATTERN, ""); -} - -function isFamilyFirstEligibleFamily(family: string) { - const normalized = family.toLowerCase(); - return normalized.startsWith("gpt-") || normalized.startsWith("claude"); -} - -export function getFamilyFirstPublishedModelId(actualModelId: string, family: string | null | undefined) { - const normalizedFamily = normalizeFamily(family); - if (!normalizedFamily || !isFamilyFirstEligibleFamily(normalizedFamily)) { - return actualModelId; - } - - const parts = splitActualModelId(actualModelId); - if (!parts) { - return actualModelId; - } - - const tierSuffix = extractTierSuffix(parts.providerModelId); - const providerModelBase = stripTierSuffix(parts.providerModelId); - if (providerModelBase !== normalizedFamily) { - return actualModelId; - } - - return `${normalizedFamily}__provider_${parts.providerPrefix}${tierSuffix}`; -} - -export function resolveFamilyFirstPublishedModelId(modelId: string | null | undefined) { - const trimmedModelId = typeof modelId === "string" ? modelId.trim() : ""; - if (!trimmedModelId) { - return trimmedModelId; - } - - const match = trimmedModelId.match(FAMILY_FIRST_MODEL_PATTERN); - if (!match) { - return trimmedModelId; - } - - const [, family, providerPrefix, serviceTier] = match; - const tierSuffix = serviceTier ? `__tier_${serviceTier.toLowerCase()}` : ""; - return `${providerPrefix}/${family}${tierSuffix}`; -} - -export function getFamilyFirstModelCandidates(actualModelId: string, family: string | null | undefined) { - const normalizedFamily = normalizeFamily(family); - const candidates = new Set([actualModelId]); - const publishedModelId = getFamilyFirstPublishedModelId(actualModelId, normalizedFamily); - if (publishedModelId !== actualModelId) { - candidates.add(publishedModelId); - } - - if (normalizedFamily && isFamilyFirstEligibleFamily(normalizedFamily)) { - const tierSuffix = extractTierSuffix(actualModelId); - candidates.add(`${normalizedFamily}${tierSuffix}`); - } - - return [...candidates]; -} \ No newline at end of file +export { + getFamilyFirstModelCandidates, + getFamilyFirstPublishedModelId, + resolveFamilyFirstPublishedModelId, +} from "@/lib/vscode/familyFirstModelIds"; diff --git a/src/app/api/v1/vscode/raw/[token]/modelPresentation.ts b/src/app/api/v1/vscode/raw/[token]/modelPresentation.ts index ca632e6902..be09ed3bf6 100644 --- a/src/app/api/v1/vscode/raw/[token]/modelPresentation.ts +++ b/src/app/api/v1/vscode/raw/[token]/modelPresentation.ts @@ -1,177 +1,5 @@ -import { parseModel } from "@omniroute/open-sse/services/model"; -import { - getCanonicalModelMetadata, - type CanonicalModelMetadata, -} from "@/lib/modelMetadataRegistry"; -import { - getVscodeServiceTierVariantSuffix, - parseVscodeServiceTierVariantModelId, - supportsVscodeServiceTierVariants, -} from "@/app/api/v1/vscode/raw/[token]/serviceTierVariants"; -import { getReasoningVariantBaseModelId } from "@/app/api/v1/vscode/raw/[token]/reasoningMetadata"; -import { resolveFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/raw/[token]/familyFirstModelIds"; -type VscodeCatalogModel = { - id?: string; - name?: string; - root?: string; - owned_by?: string; -}; - -const PROVIDER_NAME_OVERRIDES: Record = { - codex: "Codex", - cx: "Codex", - github: "GitHub", - gh: "GitHub", - gemini: "Gemini", -}; - -function tokenize(value: string) { - return value - .toLowerCase() - .split(/[^a-z0-9]+/i) - .map((part) => part.trim()) - .filter((part) => part.length >= 4); -} - -function getProviderPrefix(metadata: CanonicalModelMetadata | null) { - const providerKey = metadata?.providerAlias || metadata?.provider || ""; - if (providerKey && PROVIDER_NAME_OVERRIDES[providerKey]) { - return PROVIDER_NAME_OVERRIDES[providerKey]; - } - - const providerLabel = metadata?.providerLabel?.trim() || null; - if (!providerLabel) { - return null; - } - if (/codex/i.test(providerLabel)) { - return "Codex"; - } - if (/github/i.test(providerLabel)) { - return "GitHub"; - } - if (/gemini/i.test(providerLabel)) { - return "Gemini"; - } - - return providerLabel; -} - -function normalizeDisplayNameBranding(displayName: string) { - return displayName - .replace(/^OpenAI\s+Codex\b/i, "Codex") - .replace(/^GitHub\s+Copilot\b/i, "GitHub") - .trim(); -} - -function stripLeadingProviderPrefix(displayName: string, providerPrefix: string | null) { - if (!providerPrefix) { - return displayName; - } - - const escapedProviderPrefix = providerPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - return displayName.replace(new RegExp(`^${escapedProviderPrefix}\\s+`, "i"), "").trim(); -} - -function looksLikeTechnicalModelName(value: string) { - return /\/|__provider_|__tier_|^[a-z0-9-]+\/[a-z0-9._-]+$/i.test(value); -} - -function humanizeModelIdentifier(modelId: string) { - const identifier = (modelId.split("/").pop() || modelId).trim(); - if (!identifier) { - return identifier; - } - - return identifier - .split(/[-_]+/) - .filter(Boolean) - .map((part) => { - if (/^gpt$/i.test(part)) return "GPT"; - if (/^[0-9]+(?:\.[0-9]+)*$/.test(part)) return part; - if (/^[a-z][0-9]$/i.test(part)) return part.toUpperCase(); - return part.charAt(0).toUpperCase() + part.slice(1); - }) - .join(" "); -} - -function resolveFriendlyBaseDisplayName( - rawModelId: string, - metadata: CanonicalModelMetadata | null, - fallbackValue: string -) { - const normalizedFallback = normalizeDisplayNameBranding(fallbackValue.trim()); - if (normalizedFallback && !looksLikeTechnicalModelName(normalizedFallback)) { - return normalizedFallback; - } - - const normalizedModelId = resolveFamilyFirstPublishedModelId(rawModelId); - const parsedTierModel = parseVscodeServiceTierVariantModelId(normalizedModelId); - const canonicalBaseModelId = getReasoningVariantBaseModelId(parsedTierModel.baseModelId); - const parsed = parseModel(canonicalBaseModelId, ""); - const providerModelId = - parsed.model || - (canonicalBaseModelId.includes("/") - ? canonicalBaseModelId.split("/").slice(1).join("/") - : canonicalBaseModelId) || - fallbackValue; - - return humanizeModelIdentifier(providerModelId); -} - -function prefixDisplayName(displayName: string, providerPrefix: string | null) { - const normalizedProviderPrefix = providerPrefix?.trim() || null; - const normalizedDisplayName = normalizeDisplayNameBranding(displayName); - - if (!normalizedProviderPrefix) return normalizedDisplayName; - - const providerTokens = tokenize(normalizedProviderPrefix); - if (providerTokens.length === 0) return normalizedDisplayName; - - const displayNameLower = normalizedDisplayName.toLowerCase(); - if (providerTokens.some((token) => displayNameLower.includes(token))) { - return normalizedDisplayName; - } - - return `${normalizedProviderPrefix} ${stripLeadingProviderPrefix(normalizedDisplayName, normalizedProviderPrefix)}`.trim(); -} - -export function resolveVscodeModelMetadata(model: VscodeCatalogModel) { - const rawModelId = model.id || model.root || model.name || ""; - const normalizedModelId = resolveFamilyFirstPublishedModelId(rawModelId); - const parsedTierModel = parseVscodeServiceTierVariantModelId(normalizedModelId); - const canonicalBaseModelId = getReasoningVariantBaseModelId(parsedTierModel.baseModelId); - const parsed = parseModel(canonicalBaseModelId, ""); - const provider = parsed.provider || model.owned_by || undefined; - const providerModel = - parsed.model || - (canonicalBaseModelId.includes("/") - ? canonicalBaseModelId.split("/").slice(1).join("/") - : canonicalBaseModelId) || - model.root || - model.id || - model.name || - undefined; - - return providerModel && provider - ? getCanonicalModelMetadata({ provider, model: providerModel }) - : providerModel - ? getCanonicalModelMetadata({ model: providerModel }) - : null; -} - -export function getVscodeModelDisplayName(model: VscodeCatalogModel) { - const rawModelId = model.id || model.root || model.name || ""; - const { serviceTier } = parseVscodeServiceTierVariantModelId(rawModelId); - const metadata = resolveVscodeModelMetadata(model); - const displayName = metadata?.displayName || model.name || model.id || model.root || "unknown"; - const prefixedDisplayName = prefixDisplayName(displayName, getProviderPrefix(metadata)); - const shouldShowTierSuffix = Boolean(serviceTier) || supportsVscodeServiceTierVariants(model); - return shouldShowTierSuffix - ? `${prefixedDisplayName} (${getVscodeServiceTierVariantSuffix(serviceTier)})` - : prefixedDisplayName; -} - -export function getVscodeModelGroupingKey(model: VscodeCatalogModel) { - const metadata = resolveVscodeModelMetadata(model); - return metadata?.qualifiedId || metadata?.model || model.id || model.name || model.root || ""; -} +export { + getVscodeModelDisplayName, + getVscodeModelGroupingKey, + resolveVscodeModelMetadata, +} from "@/lib/vscode/modelPresentation"; diff --git a/src/app/api/v1/vscode/raw/[token]/reasoningMetadata.ts b/src/app/api/v1/vscode/raw/[token]/reasoningMetadata.ts index 431bc25a4c..3153457309 100644 --- a/src/app/api/v1/vscode/raw/[token]/reasoningMetadata.ts +++ b/src/app/api/v1/vscode/raw/[token]/reasoningMetadata.ts @@ -1,175 +1,13 @@ -import { parseModel } from "@omniroute/open-sse/services/model"; -import { supportsXHighEffort } from "@omniroute/open-sse/config/providerModels"; -import { stripVscodeServiceTierVariantModelId } from "@/app/api/v1/vscode/raw/[token]/serviceTierVariants"; - -export type VscodeCatalogModel = { - id?: string; - name?: string; - root?: string; - owned_by?: string; - capabilities?: Record; - supportsReasoningEffort?: string[]; - supportedReasoningEfforts?: string[]; - supports_reasoning_effort?: string[]; - defaultReasoningEffort?: string; - default_reasoning_effort?: string; -}; - -const EFFORT_SUFFIX_PATTERN = /-(xhigh|high|medium|low|none)$/i; -const DEFAULT_REASONING_EFFORT = "none"; -const KNOWN_REASONING_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh"]); - -export type VscodeModelConfigSchema = { - type: "object"; - properties: { - reasoningEffort: { - type: "string"; - title: string; - description: string; - default: string; - enum: string[]; - enumLabels: string[]; - enumDescriptions: string[]; - }; - }; -}; - -export function getCatalogModelName(model: VscodeCatalogModel) { - return stripVscodeServiceTierVariantModelId(model.id || model.name || model.root || ""); -} - -function normalizeReasoningEffortValue(value: string) { - const normalized = value.trim().toLowerCase().replace(/[_\s-]+/g, ""); - if (normalized === "xhigh") return "xhigh"; - if (KNOWN_REASONING_EFFORTS.has(normalized)) return normalized; - return undefined; -} - -function getNativeReasoningEffortValues(model: VscodeCatalogModel) { - const candidates = [ - model.supportsReasoningEffort, - model.supportedReasoningEfforts, - model.supports_reasoning_effort, - ]; - - for (const candidate of candidates) { - if (!Array.isArray(candidate) || candidate.length === 0) { - continue; - } - - const normalized = Array.from( - new Set(candidate.map(value => typeof value === "string" ? normalizeReasoningEffortValue(value) : undefined).filter(Boolean)) - ) as string[]; - - if (normalized.length > 0) { - return normalized; - } - } - - return undefined; -} - -export function isReasoningCapableModel(model: VscodeCatalogModel) { - return ( - model.capabilities?.reasoning === true || - model.capabilities?.thinking === true || - (getNativeReasoningEffortValues(model)?.length || 0) > 0 - ); -} - -export function getReasoningEffortValues(model: VscodeCatalogModel) { - const nativeReasoningEffortValues = getNativeReasoningEffortValues(model); - if (nativeReasoningEffortValues && nativeReasoningEffortValues.length > 0) { - return nativeReasoningEffortValues; - } - - if (!isReasoningCapableModel(model)) return undefined; - - const modelId = getCatalogModelName(model); - const parsed = parseModel(modelId, ""); - const providerId = parsed.provider || model.owned_by || ""; - const providerModelId = parsed.model || model.root || modelId.split("/").pop() || modelId; - const values = ["none", "low", "medium", "high"]; - - if (providerId && providerModelId && supportsXHighEffort(providerId, providerModelId)) { - values.push("xhigh"); - } - - return values; -} - -export function formatReasoningEffortLabel(level: string) { - if (level === "xhigh") return "XHigh"; - return level.charAt(0).toUpperCase() + level.slice(1); -} - -function describeReasoningEffort(level: string) { - switch (level) { - case "none": - return "Disables extra reasoning effort."; - case "low": - return "Uses a light amount of reasoning."; - case "medium": - return "Uses a balanced amount of reasoning."; - case "high": - return "Uses an extended amount of reasoning."; - case "xhigh": - return "Uses the maximum available reasoning effort."; - default: - return `Uses ${formatReasoningEffortLabel(level)} reasoning effort.`; - } -} - -export function buildSupportedReasoningEfforts( - supportedValues: string[] -): string[] { - return [...supportedValues]; -} - -export function inferSelectedReasoningEffort( - model: VscodeCatalogModel, - supportedValues?: string[] -) { - const modelId = getCatalogModelName(model); - const match = modelId.match(EFFORT_SUFFIX_PATTERN); - if (!match) return undefined; - - const selected = match[1]?.toLowerCase(); - if (!selected) return undefined; - if (Array.isArray(supportedValues) && supportedValues.length > 0 && !supportedValues.includes(selected)) { - return undefined; - } - - return selected; -} - -export function getReasoningVariantBaseModelId(modelId: string) { - return modelId.replace(EFFORT_SUFFIX_PATTERN, ""); -} - -export function getDefaultReasoningEffort( - model: VscodeCatalogModel, - supportedValues?: string[] -) { - return inferSelectedReasoningEffort(model, supportedValues) || DEFAULT_REASONING_EFFORT; -} - -export function buildReasoningConfigSchema( - supportedValues: string[], - defaultReasoningEffort: string -): VscodeModelConfigSchema { - return { - type: "object", - properties: { - reasoningEffort: { - type: "string", - title: "Reasoning effort", - description: "Controls how much reasoning effort the model uses.", - default: defaultReasoningEffort, - enum: supportedValues, - enumLabels: supportedValues.map(formatReasoningEffortLabel), - enumDescriptions: supportedValues.map(describeReasoningEffort), - }, - }, - }; -} +export { + buildReasoningConfigSchema, + buildSupportedReasoningEfforts, + formatReasoningEffortLabel, + getCatalogModelName, + getDefaultReasoningEffort, + getReasoningEffortValues, + getReasoningVariantBaseModelId, + inferSelectedReasoningEffort, + isReasoningCapableModel, + type VscodeCatalogModel, + type VscodeModelConfigSchema, +} from "@/lib/vscode/reasoningMetadata"; diff --git a/src/app/api/v1/vscode/raw/[token]/serviceTierVariants.ts b/src/app/api/v1/vscode/raw/[token]/serviceTierVariants.ts index 538354a839..08bfb6ef3b 100644 --- a/src/app/api/v1/vscode/raw/[token]/serviceTierVariants.ts +++ b/src/app/api/v1/vscode/raw/[token]/serviceTierVariants.ts @@ -1,190 +1,12 @@ -import { CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS } from "@/lib/providers/codexFastTier"; -import { normalizeServiceTierId, type ServiceTierId } from "@/shared/utils/serviceTierLabels"; -import { resolveFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/raw/[token]/familyFirstModelIds"; - -const SERVICE_TIER_VARIANT_PATTERN = /__tier_(priority|flex)$/i; -const SUPPORTED_VSCODE_SERVICE_TIERS: readonly ServiceTierId[] = ["priority", "flex"]; - -export type VscodeServiceTierModelLike = { - id?: string; - name?: string; - root?: string; - owned_by?: string; -}; - -export function parseVscodeServiceTierVariantModelId(modelId: string | null | undefined): { - baseModelId: string; - serviceTier?: ServiceTierId; -} { - const rawModelId = typeof modelId === "string" ? modelId.trim() : ""; - if (!rawModelId) { - return { baseModelId: "" }; - } - - const match = rawModelId.match(SERVICE_TIER_VARIANT_PATTERN); - if (!match) { - return { baseModelId: rawModelId }; - } - - const baseModelId = rawModelId.replace(SERVICE_TIER_VARIANT_PATTERN, ""); - const serviceTier = normalizeServiceTierId(match[1]); - return serviceTier === "standard" ? { baseModelId } : { baseModelId, serviceTier }; -} - -export function stripVscodeServiceTierVariantModelId(modelId: string | null | undefined): string { - return parseVscodeServiceTierVariantModelId(modelId).baseModelId; -} - -export function isVscodeServiceTierVariantModelId(modelId: string | null | undefined): boolean { - return Boolean(parseVscodeServiceTierVariantModelId(modelId).serviceTier); -} - -export function getVscodeServiceTierVariantModelId( - baseModelId: string, - serviceTier: ServiceTierId -): string { - if (serviceTier === "standard") { - return baseModelId; - } - return `${baseModelId}__tier_${serviceTier}`; -} - -function getRawModelId(model: VscodeServiceTierModelLike): string { - return (model.id || model.name || model.root || "").trim(); -} - -function getModelProvider(model: VscodeServiceTierModelLike, baseModelId: string): string { - const owner = typeof model.owned_by === "string" ? model.owned_by.trim().toLowerCase() : ""; - if (owner) { - return owner; - } - const prefix = baseModelId.split("/")[0]?.trim().toLowerCase() || ""; - return prefix; -} - -function supportsCodexServiceTierModel(baseModelId: string): boolean { - const normalizedModel = (baseModelId.split("/").pop() || baseModelId).trim().toLowerCase(); - if (!normalizedModel) { - return false; - } - - return CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS.some((candidate) => { - const normalizedCandidate = candidate.trim().toLowerCase(); - return normalizedModel === normalizedCandidate || normalizedModel.startsWith(normalizedCandidate); - }); -} - -export function supportsVscodeServiceTierVariants(model: VscodeServiceTierModelLike): boolean { - const rawModelId = getRawModelId(model); - if (!rawModelId) { - return false; - } - - const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId); - const provider = getModelProvider(model, baseModelId); - if (provider !== "codex" && provider !== "cx") { - return false; - } - - return supportsCodexServiceTierModel(baseModelId); -} - -function cloneModelIdentifiers( - model: T, - modelId: string -): T { - return { - ...model, - ...(model.id ? { id: modelId } : {}), - ...(model.name ? { name: modelId } : {}), - ...(model.root ? { root: modelId } : {}), - }; -} - -export function expandVscodeServiceTierModels(models: T[]): T[] { - const expanded: T[] = []; - - for (const model of models) { - const rawModelId = getRawModelId(model); - if (!rawModelId) { - expanded.push(model); - continue; - } - - const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId); - const baseModel = rawModelId === baseModelId ? model : cloneModelIdentifiers(model, baseModelId); - expanded.push(baseModel as T); - - if (!supportsVscodeServiceTierVariants(model)) { - continue; - } - - for (const serviceTier of SUPPORTED_VSCODE_SERVICE_TIERS) { - expanded.push(cloneModelIdentifiers(baseModel as T, getVscodeServiceTierVariantModelId(baseModelId, serviceTier))); - } - } - - return expanded; -} - -export function getVscodeServiceTierVariantSuffix(serviceTier: ServiceTierId | undefined): string { - if (serviceTier === "priority") { - return "Fast"; - } - if (serviceTier === "flex") { - return "Flex"; - } - return "Default"; -} - -export function resolveVscodeServiceTierRequest(body: Record): Record { - const rawModelId = typeof body.model === "string" ? body.model.trim() : ""; - if (!rawModelId) { - return body; - } - - const resolvedModelId = resolveFamilyFirstPublishedModelId(rawModelId); - - const { baseModelId, serviceTier } = parseVscodeServiceTierVariantModelId(resolvedModelId); - if (!serviceTier) { - if (resolvedModelId === rawModelId) { - return body; - } - - return { - ...body, - model: resolvedModelId, - }; - } - - return { - ...body, - model: baseModelId, - ...(body.service_tier === undefined ? { service_tier: serviceTier } : {}), - }; -} - -export async function rewriteVscodeServiceTierRequest(request: Request): Promise { - if (request.method !== "POST") { - return request; - } - - const body = await request.clone().json().catch(() => null); - if (!body || typeof body !== "object" || Array.isArray(body)) { - return request; - } - - const rewrittenBody = resolveVscodeServiceTierRequest(body as Record); - if (rewrittenBody === body) { - return request; - } - - const headers = new Headers(request.headers); - headers.delete("content-length"); - - return new Request(request.url, { - method: request.method, - headers, - body: JSON.stringify(rewrittenBody), - }); -} +export { + expandVscodeServiceTierModels, + getVscodeServiceTierVariantModelId, + getVscodeServiceTierVariantSuffix, + isVscodeServiceTierVariantModelId, + parseVscodeServiceTierVariantModelId, + resolveVscodeServiceTierRequest, + rewriteVscodeServiceTierRequest, + stripVscodeServiceTierVariantModelId, + supportsVscodeServiceTierVariants, + type VscodeServiceTierModelLike, +} from "@/lib/vscode/serviceTierVariants"; diff --git a/src/app/api/v1/vscode/raw/[token]/tokenizedRequest.ts b/src/app/api/v1/vscode/raw/[token]/tokenizedRequest.ts index 24bb41d252..bb48fde7e7 100644 --- a/src/app/api/v1/vscode/raw/[token]/tokenizedRequest.ts +++ b/src/app/api/v1/vscode/raw/[token]/tokenizedRequest.ts @@ -1,56 +1 @@ -import { sanitizeVscodeRequest } from "@/app/api/v1/vscode/contextSanitizer"; - -function inferTokenFromVscodePath(request: Request) { - try { - const url = new URL(request.url, "http://localhost"); - const segments = url.pathname - .split("/") - .map((segment) => segment.trim()) - .filter(Boolean); - - const vscodeIndex = segments.indexOf("vscode"); - if (vscodeIndex === -1) return null; - - const candidate = segments[vscodeIndex + 1]; - if (!candidate || candidate === "raw" || candidate === "combos") { - const nestedCandidate = segments[vscodeIndex + 2]; - return nestedCandidate ? decodeURIComponent(nestedCandidate) : null; - } - - return decodeURIComponent(candidate); - } catch { - return null; - } -} - -export function withPathTokenApiKey(request: Request, token?: string) { - const resolvedToken = token || inferTokenFromVscodePath(request); - if (!resolvedToken) return request; - - const headers = new Headers(request.headers); - - if (!headers.has("x-api-key")) { - headers.set("x-api-key", resolvedToken); - } - - if (!headers.has("authorization")) { - headers.set("authorization", `Bearer ${resolvedToken}`); - } - - const method = request.method; - const init: RequestInit & { duplex?: "half" } = { - method, - headers, - }; - - if (method !== "GET" && method !== "HEAD") { - init.body = request.body; - init.duplex = "half"; - } - - return new Request(request.url, init); -} - -export async function withSanitizedPathTokenApiKey(request: Request, token?: string) { - return sanitizeVscodeRequest(withPathTokenApiKey(request, token)); -} +export { withPathTokenApiKey, withSanitizedPathTokenApiKey } from "@/lib/vscode/tokenizedRequest"; diff --git a/src/app/api/version-manager/request.ts b/src/app/api/version-manager/request.ts new file mode 100644 index 0000000000..a77cbc7c39 --- /dev/null +++ b/src/app/api/version-manager/request.ts @@ -0,0 +1,51 @@ +import { NextResponse } from "next/server"; + +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; +import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; +import { versionManagerToolSchema } from "@/shared/validation/schemas"; + +export const VERSION_MANAGER_SUPERVISOR_TOOLS = new Set(["cliproxy", "cliproxyapi"]); + +type VersionManagerToolRequest = { ok: true; tool: string } | { ok: false; response: Response }; + +export function validateVersionManagerToolBody(rawBody: unknown): VersionManagerToolRequest { + const validation = validateBody(versionManagerToolSchema, rawBody); + if (isValidationFailure(validation)) { + return { + ok: false, + response: NextResponse.json({ error: validation.error }, { status: 400 }), + }; + } + + const { tool } = validation.data; + + if (!VERSION_MANAGER_SUPERVISOR_TOOLS.has(tool)) { + return { + ok: false, + response: NextResponse.json({ error: `Unknown tool: ${tool}` }, { status: 400 }), + }; + } + + return { ok: true, tool }; +} + +export async function parseVersionManagerToolRequest( + request: Request +): Promise { + const authError = await requireManagementAuth(request); + if (authError) { + return { ok: false, response: authError }; + } + + let rawBody: unknown; + try { + rawBody = await request.json(); + } catch { + return { + ok: false, + response: NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }), + }; + } + + return validateVersionManagerToolBody(rawBody); +} diff --git a/src/app/api/version-manager/restart/route.ts b/src/app/api/version-manager/restart/route.ts index 0a57be8390..268a42f36e 100644 --- a/src/app/api/version-manager/restart/route.ts +++ b/src/app/api/version-manager/restart/route.ts @@ -3,33 +3,14 @@ import { NextResponse } from "next/server"; import { getServiceRow } from "@/lib/db/versionManager"; import { getOrInitSupervisor } from "@/app/api/services/cliproxy/_lib"; -import { versionManagerToolSchema } from "@/shared/validation/schemas"; -import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; -import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; -const SUPERVISOR_TOOLS = new Set(["cliproxy", "cliproxyapi"]); +import { parseVersionManagerToolRequest } from "../request"; export async function POST(request: Request) { - const authError = await requireManagementAuth(request); - if (authError) return authError; - - let rawBody; - try { - rawBody = await request.json(); - } catch { - return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); - } - - const validation = validateBody(versionManagerToolSchema, rawBody); - if (isValidationFailure(validation)) { - return NextResponse.json({ error: validation.error }, { status: 400 }); - } - - const { tool } = validation.data; - - if (!SUPERVISOR_TOOLS.has(tool)) { - return NextResponse.json({ error: `Unknown tool: ${tool}` }, { status: 400 }); + const parsed = await parseVersionManagerToolRequest(request); + if (!parsed.ok) { + return parsed.response; } try { diff --git a/src/app/api/version-manager/start/route.ts b/src/app/api/version-manager/start/route.ts index 1aec255376..14b13397c9 100644 --- a/src/app/api/version-manager/start/route.ts +++ b/src/app/api/version-manager/start/route.ts @@ -3,35 +3,14 @@ import { NextResponse } from "next/server"; import { getServiceRow } from "@/lib/db/versionManager"; import { getOrInitSupervisor } from "@/app/api/services/cliproxy/_lib"; -import { versionManagerToolSchema } from "@/shared/validation/schemas"; -import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; -import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; -// Only "cliproxy" has a supervisor; for other tools the legacy path is not -// available in this branch, so we return 409 if the tool is not installed. -const SUPERVISOR_TOOLS = new Set(["cliproxy", "cliproxyapi"]); +import { parseVersionManagerToolRequest } from "../request"; export async function POST(request: Request) { - const authError = await requireManagementAuth(request); - if (authError) return authError; - - let rawBody; - try { - rawBody = await request.json(); - } catch { - return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); - } - - const validation = validateBody(versionManagerToolSchema, rawBody); - if (isValidationFailure(validation)) { - return NextResponse.json({ error: validation.error }, { status: 400 }); - } - - const { tool } = validation.data; - - if (!SUPERVISOR_TOOLS.has(tool)) { - return NextResponse.json({ error: `Unknown tool: ${tool}` }, { status: 400 }); + const parsed = await parseVersionManagerToolRequest(request); + if (!parsed.ok) { + return parsed.response; } try { diff --git a/src/app/api/version-manager/stop/route.ts b/src/app/api/version-manager/stop/route.ts index 37d82c16ed..2a10177529 100644 --- a/src/app/api/version-manager/stop/route.ts +++ b/src/app/api/version-manager/stop/route.ts @@ -2,33 +2,14 @@ import { NextResponse } from "next/server"; import { getSupervisor } from "@/lib/services/registry"; -import { versionManagerToolSchema } from "@/shared/validation/schemas"; -import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; -import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; -const SUPERVISOR_TOOLS = new Set(["cliproxy", "cliproxyapi"]); +import { parseVersionManagerToolRequest } from "../request"; export async function POST(request: Request) { - const authError = await requireManagementAuth(request); - if (authError) return authError; - - let rawBody; - try { - rawBody = await request.json(); - } catch { - return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); - } - - const validation = validateBody(versionManagerToolSchema, rawBody); - if (isValidationFailure(validation)) { - return NextResponse.json({ error: validation.error }, { status: 400 }); - } - - const { tool } = validation.data; - - if (!SUPERVISOR_TOOLS.has(tool)) { - return NextResponse.json({ error: `Unknown tool: ${tool}` }, { status: 400 }); + const parsed = await parseVersionManagerToolRequest(request); + if (!parsed.ok) { + return parsed.response; } try { diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 29cff11693..8029041ba5 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -4497,9 +4497,9 @@ "a": "A", "accountConcurrencyCapHint": "Max concurrent requests this connection/account can handle. Subscription accounts usually allow only a few (1–3) — going over triggers 429s and cooldowns. Leave empty for the default.", "accountConcurrencyCapLabel": "Account concurrency cap", - "accountIdHint": "Account Id Hint", - "accountIdLabel": "Account Id Label", - "accountIdPlaceholder": "Account Id Placeholder", + "accountIdHint": "Required by some providers (e.g. Cloudflare Workers AI) alongside the API token.", + "accountIdLabel": "Account ID", + "accountIdPlaceholder": "Your provider account ID", "addAnotherApiKey": "Add another API key or paste multiple keys", "addCcCompatible": "Add CC Compatible", "aggregatorsGateways": "Aggregators Gateways", @@ -4675,9 +4675,9 @@ "totalKeysRotating": "{count, plural, one {1 key rotating} other {# keys rotating}}", "unhideModel": "Unhide Model", "upstreamProxyProviders": "Upstream Proxy Providers", - "validationModelIdHint": "Validation Model Id Hint", - "validationModelIdLabel": "Validation Model Id Label", - "validationModelIdPlaceholder": "Validation Model Id Placeholder", + "validationModelIdHint": "Model used to verify the API key. Leave blank to use the provider\u2019s first available model.", + "validationModelIdLabel": "Validation Model", + "validationModelIdPlaceholder": "e.g. meta-llama/llama-3.1-8b-instruct", "vertexServiceAccountPlaceholder": "Paste your Service Account JSON ({\"type\":\"service_account\",\"project_id\":\"…\",\"client_email\":\"…\",\"private_key\":\"…\"}) or an OAuth access_token", "webCookieProviders": "Web Cookie Providers", "weeklyShort": "Weekly Short", @@ -4818,8 +4818,6 @@ "checkWebToken": "Check token", "huggingchatLabel": "HuggingChat (Free)", "huggingchatDesc": "Free LLM chat via huggingface.co/chat", - "phindLabel": "Phind (Free)", - "phindDesc": "Free dev-focused AI chat", "poeWebLabel": "Poe Web", "poeWebDesc": "Multi-model chat via poe.com", "veniceWebLabel": "Venice Web", @@ -5639,6 +5637,7 @@ "compressionPreserveSystem": "Preserve System Prompt", "compressionCavemanConfig": "Caveman Engine Configuration", "compressionCavemanConfigDesc": "Fine-tune the rule-based compression engine", + "compressionCavemanPanelHint": "Its on/off and level are set in the panel:", "compressionRoles": "Compress Message Roles", "compressionRoleUser": "User", "compressionRoleAssistant": "Assistant", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 90da32b420..b8a544b12f 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -4804,8 +4804,6 @@ "checkWebToken": "Controlla il gettone", "huggingchatLabel": "__MISSING__:HuggingChat (Free)", "huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat", - "phindLabel": "__MISSING__:Phind (Free)", - "phindDesc": "__MISSING__:Free dev-focused AI chat", "poeWebLabel": "__MISSING__:Poe Web", "poeWebDesc": "__MISSING__:Multi-model chat via poe.com", "veniceWebLabel": "__MISSING__:Venice Web", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 9ea5764886..418afb8712 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -4804,8 +4804,6 @@ "checkWebToken": "トークンをチェックする", "huggingchatLabel": "__MISSING__:HuggingChat (Free)", "huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat", - "phindLabel": "__MISSING__:Phind (Free)", - "phindDesc": "__MISSING__:Free dev-focused AI chat", "poeWebLabel": "__MISSING__:Poe Web", "poeWebDesc": "__MISSING__:Multi-model chat via poe.com", "veniceWebLabel": "__MISSING__:Venice Web", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 2856459b1a..3185cf6f7d 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -4804,8 +4804,6 @@ "checkWebToken": "토큰 확인", "huggingchatLabel": "__MISSING__:HuggingChat (Free)", "huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat", - "phindLabel": "__MISSING__:Phind (Free)", - "phindDesc": "__MISSING__:Free dev-focused AI chat", "poeWebLabel": "__MISSING__:Poe Web", "poeWebDesc": "__MISSING__:Multi-model chat via poe.com", "veniceWebLabel": "__MISSING__:Venice Web", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index f32ca30f44..619f356739 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -4804,8 +4804,6 @@ "checkWebToken": "टोकन तपासा", "huggingchatLabel": "__MISSING__:HuggingChat (Free)", "huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat", - "phindLabel": "__MISSING__:Phind (Free)", - "phindDesc": "__MISSING__:Free dev-focused AI chat", "poeWebLabel": "__MISSING__:Poe Web", "poeWebDesc": "__MISSING__:Multi-model chat via poe.com", "veniceWebLabel": "__MISSING__:Venice Web", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index 7a8bfabdfb..f77759ef3a 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -4804,8 +4804,6 @@ "checkWebToken": "Semak token", "huggingchatLabel": "__MISSING__:HuggingChat (Free)", "huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat", - "phindLabel": "__MISSING__:Phind (Free)", - "phindDesc": "__MISSING__:Free dev-focused AI chat", "poeWebLabel": "__MISSING__:Poe Web", "poeWebDesc": "__MISSING__:Multi-model chat via poe.com", "veniceWebLabel": "__MISSING__:Venice Web", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 3d7b367ddf..9c2d5c7a2e 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -4804,8 +4804,6 @@ "checkWebToken": "Controleer token", "huggingchatLabel": "__MISSING__:HuggingChat (Free)", "huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat", - "phindLabel": "__MISSING__:Phind (Free)", - "phindDesc": "__MISSING__:Free dev-focused AI chat", "poeWebLabel": "__MISSING__:Poe Web", "poeWebDesc": "__MISSING__:Multi-model chat via poe.com", "veniceWebLabel": "__MISSING__:Venice Web", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index 9251bbb62f..1d2a66826a 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -4804,8 +4804,6 @@ "checkWebToken": "Sjekk token", "huggingchatLabel": "__MISSING__:HuggingChat (Free)", "huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat", - "phindLabel": "__MISSING__:Phind (Free)", - "phindDesc": "__MISSING__:Free dev-focused AI chat", "poeWebLabel": "__MISSING__:Poe Web", "poeWebDesc": "__MISSING__:Multi-model chat via poe.com", "veniceWebLabel": "__MISSING__:Venice Web", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index d185e38eeb..20903e812e 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -4804,8 +4804,6 @@ "checkWebToken": "Suriin ang token", "huggingchatLabel": "__MISSING__:HuggingChat (Free)", "huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat", - "phindLabel": "__MISSING__:Phind (Free)", - "phindDesc": "__MISSING__:Free dev-focused AI chat", "poeWebLabel": "__MISSING__:Poe Web", "poeWebDesc": "__MISSING__:Multi-model chat via poe.com", "veniceWebLabel": "__MISSING__:Venice Web", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 0d85a35810..105eabf5d0 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -4804,8 +4804,6 @@ "checkWebToken": "Sprawdź token", "huggingchatLabel": "__MISSING__:HuggingChat (Free)", "huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat", - "phindLabel": "__MISSING__:Phind (Free)", - "phindDesc": "__MISSING__:Free dev-focused AI chat", "poeWebLabel": "__MISSING__:Poe Web", "poeWebDesc": "__MISSING__:Multi-model chat via poe.com", "veniceWebLabel": "__MISSING__:Venice Web", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 6a176be8e5..805d8c03a9 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -4804,8 +4804,6 @@ "checkWebToken": "Verifique o token", "huggingchatLabel": "__MISSING__:HuggingChat (Free)", "huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat", - "phindLabel": "__MISSING__:Phind (Free)", - "phindDesc": "__MISSING__:Free dev-focused AI chat", "poeWebLabel": "__MISSING__:Poe Web", "poeWebDesc": "__MISSING__:Multi-model chat via poe.com", "veniceWebLabel": "__MISSING__:Venice Web", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 6970574c44..8bcae514c7 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -4804,8 +4804,6 @@ "checkWebToken": "Verifique o token", "huggingchatLabel": "__MISSING__:HuggingChat (Free)", "huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat", - "phindLabel": "__MISSING__:Phind (Free)", - "phindDesc": "__MISSING__:Free dev-focused AI chat", "poeWebLabel": "__MISSING__:Poe Web", "poeWebDesc": "__MISSING__:Multi-model chat via poe.com", "veniceWebLabel": "__MISSING__:Venice Web", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index 20d7e28ed0..2668ee7132 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -4804,8 +4804,6 @@ "checkWebToken": "Verificați jetonul", "huggingchatLabel": "__MISSING__:HuggingChat (Free)", "huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat", - "phindLabel": "__MISSING__:Phind (Free)", - "phindDesc": "__MISSING__:Free dev-focused AI chat", "poeWebLabel": "__MISSING__:Poe Web", "poeWebDesc": "__MISSING__:Multi-model chat via poe.com", "veniceWebLabel": "__MISSING__:Venice Web", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index e35c7ed60c..2c5c0f7568 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -4804,8 +4804,6 @@ "checkWebToken": "Проверить токен", "huggingchatLabel": "HuggingChat (Бесплатно)", "huggingchatDesc": "Бесплатный LLM чат через huggingface.co/chat", - "phindLabel": "Phind (Бесплатно)", - "phindDesc": "Бесплатный AI-чат для разработчиков", "poeWebLabel": "Poe Web", "poeWebDesc": "Многофункциональный чат через poe.com", "veniceWebLabel": "Венеция Веб", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index 5bcb0b2e13..bf043d2359 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -4804,8 +4804,6 @@ "checkWebToken": "Kontrolný token", "huggingchatLabel": "__MISSING__:HuggingChat (Free)", "huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat", - "phindLabel": "__MISSING__:Phind (Free)", - "phindDesc": "__MISSING__:Free dev-focused AI chat", "poeWebLabel": "__MISSING__:Poe Web", "poeWebDesc": "__MISSING__:Multi-model chat via poe.com", "veniceWebLabel": "__MISSING__:Venice Web", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index 566b27dcd0..3639ea65d6 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -4804,8 +4804,6 @@ "checkWebToken": "Kontrollera token", "huggingchatLabel": "__MISSING__:HuggingChat (Free)", "huggingchatDesc": "__MISSING__:Free LLM chat via huggingface.co/chat", - "phindLabel": "__MISSING__:Phind (Free)", - "phindDesc": "__MISSING__:Free dev-focused AI chat", "poeWebLabel": "__MISSING__:Poe Web", "poeWebDesc": "__MISSING__:Multi-model chat via poe.com", "veniceWebLabel": "__MISSING__:Venice Web", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 6b22afbf50..df4cedc1ad 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -4660,8 +4660,6 @@ "checkWebToken": "检查令牌", "huggingchatLabel": "HuggingChat(免费)", "huggingchatDesc": "通过 huggingface.co/chat 免费使用 LLM 聊天", - "phindLabel": "Phind(免费)", - "phindDesc": "免费的开发者专用 AI 聊天", "poeWebLabel": "Poe 网页", "poeWebDesc": "通过 poe.com 进行多模型聊天", "veniceWebLabel": "威尼斯网络", diff --git a/src/lib/a2a/routingLogger.ts b/src/lib/a2a/routingLogger.ts index de1b701ec9..45468bce58 100644 --- a/src/lib/a2a/routingLogger.ts +++ b/src/lib/a2a/routingLogger.ts @@ -65,52 +65,3 @@ export function logRoutingDecision( return decision; } - -/** - * Get a specific routing decision by request ID. - */ -export function getRoutingDecision(requestId: string): RoutingDecision | undefined { - return decisions.find((d) => d.requestId === requestId); -} - -/** - * Get recent routing decisions. - */ -export function getRecentDecisions(limit: number = 20): RoutingDecision[] { - return decisions.slice(-limit).reverse(); -} - -/** - * Get routing decision stats. - */ -export function getDecisionStats(): { - total: number; - successRate: number; - avgLatencyMs: number; - topProviders: Array<{ provider: string; count: number }>; -} { - if (decisions.length === 0) { - return { total: 0, successRate: 1, avgLatencyMs: 0, topProviders: [] }; - } - - const successful = decisions.filter((d) => d.success); - const providerCounts = new Map(); - let totalLatency = 0; - - for (const d of decisions) { - totalLatency += d.latencyMs; - providerCounts.set(d.providerSelected, (providerCounts.get(d.providerSelected) || 0) + 1); - } - - const topProviders = [...providerCounts.entries()] - .sort((a, b) => b[1] - a[1]) - .slice(0, 5) - .map(([provider, count]) => ({ provider, count })); - - return { - total: decisions.length, - successRate: successful.length / decisions.length, - avgLatencyMs: Math.round(totalLatency / decisions.length), - topProviders, - }; -} diff --git a/src/lib/agentSkills/schemas.ts b/src/lib/agentSkills/schemas.ts index f6538a1f90..0f77d66ba3 100644 --- a/src/lib/agentSkills/schemas.ts +++ b/src/lib/agentSkills/schemas.ts @@ -34,8 +34,3 @@ export const GenerateBodySchema = z.object({ prune: z.boolean().default(false), onlyIds: z.array(z.string()).optional(), }); - -export type AgentSkillT = z.infer; -export type SkillCoverageT = z.infer; -export type ListQueryT = z.infer; -export type GenerateBodyT = z.infer; diff --git a/src/lib/api/proxyRegistryRouteHandlers.ts b/src/lib/api/proxyRegistryRouteHandlers.ts new file mode 100644 index 0000000000..dc93873956 --- /dev/null +++ b/src/lib/api/proxyRegistryRouteHandlers.ts @@ -0,0 +1,142 @@ +import { + createProxy, + createProxyAndAssign, + deleteProxyById, + getProxyById, + getProxyWhereUsed, + updateProxy, + updateProxyAndAssign, +} from "@/lib/localDb"; +import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse"; +import { createProxyRegistrySchema, updateProxyRegistrySchema } from "@/shared/validation/schemas"; +import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; +import { clearDispatcherCache } from "@omniroute/open-sse/utils/proxyDispatcher"; + +async function readJsonBody(request: Request) { + try { + return { ok: true as const, body: await request.json() }; + } catch { + return { + ok: false as const, + response: createErrorResponse({ + status: 400, + message: "Invalid JSON body", + type: "invalid_request", + }), + }; + } +} + +export async function resolveProxyLookupResponse( + searchParams: URLSearchParams, + whereUsedParam: string +): Promise { + const id = searchParams.get("id"); + const whereUsed = searchParams.get(whereUsedParam) === "1"; + + if (id && whereUsed) { + const usage = await getProxyWhereUsed(id); + return Response.json(usage); + } + + if (!id) { + return null; + } + + const proxy = await getProxyById(id, { includeSecrets: false }); + if (!proxy) { + return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" }); + } + return Response.json(proxy); +} + +export async function handleProxyCreate(request: Request) { + const parsed = await readJsonBody(request); + if (!parsed.ok) return parsed.response; + + try { + const validation = validateBody(createProxyRegistrySchema, parsed.body); + if (isValidationFailure(validation)) { + return createErrorResponse({ + status: 400, + message: validation.error.message, + details: validation.error.details, + type: "invalid_request", + }); + } + + const { assignment, ...proxyFields } = validation.data; + if (assignment) { + const result = await createProxyAndAssign(proxyFields, assignment); + clearDispatcherCache(); + return Response.json({ ...result.proxy, assignment: result.assignment }, { status: 201 }); + } + + const created = await createProxy(proxyFields); + return Response.json(created, { status: 201 }); + } catch (error) { + return createErrorResponseFromUnknown(error, "Failed to create proxy"); + } +} + +export async function handleProxyUpdate(request: Request) { + const parsed = await readJsonBody(request); + if (!parsed.ok) return parsed.response; + + try { + const validation = validateBody(updateProxyRegistrySchema, parsed.body); + if (isValidationFailure(validation)) { + return createErrorResponse({ + status: 400, + message: validation.error.message, + details: validation.error.details, + type: "invalid_request", + }); + } + + const { id, assignment, ...changes } = validation.data; + if (assignment) { + const result = await updateProxyAndAssign(id, changes, assignment); + if (!result?.proxy) { + return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" }); + } + + clearDispatcherCache(); + return Response.json({ ...result.proxy, assignment: result.assignment }); + } + + const updated = await updateProxy(id, changes); + if (!updated) { + return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" }); + } + + return Response.json(updated); + } catch (error) { + return createErrorResponseFromUnknown(error, "Failed to update proxy"); + } +} + +export async function handleProxyDelete(request: Request) { + try { + const { searchParams } = new URL(request.url); + const id = searchParams.get("id"); + const force = searchParams.get("force") === "1"; + + if (!id) { + return createErrorResponse({ + status: 400, + message: "id is required", + type: "invalid_request", + }); + } + + const deleted = await deleteProxyById(id, { force }); + if (!deleted) { + return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" }); + } + + return Response.json({ success: true }); + } catch (error) { + return createErrorResponseFromUnknown(error, "Failed to delete proxy"); + } +} diff --git a/src/lib/arenaEloSync.ts b/src/lib/arenaEloSync.ts index c57b1106e9..1e24148716 100644 --- a/src/lib/arenaEloSync.ts +++ b/src/lib/arenaEloSync.ts @@ -132,7 +132,6 @@ const VENDOR_PREFIXES = [ "alibaba/", "nvidia/", "01-ai/", - "phind/", "zerox/", "together/", "fireworks/", diff --git a/src/lib/audit/highLevelActions.ts b/src/lib/audit/highLevelActions.ts index a76bae5763..e38b701ed1 100644 --- a/src/lib/audit/highLevelActions.ts +++ b/src/lib/audit/highLevelActions.ts @@ -50,8 +50,6 @@ export const HIGH_LEVEL_ACTIONS = [ "quota.store.driver_changed", ] as const; -export type HighLevelAction = (typeof HIGH_LEVEL_ACTIONS)[number]; - const SET: ReadonlySet = new Set(HIGH_LEVEL_ACTIONS); export function isHighLevelAction(action: string): boolean { diff --git a/src/lib/batches/types.ts b/src/lib/batches/types.ts index 9c2154e0d0..9b62dbda65 100644 --- a/src/lib/batches/types.ts +++ b/src/lib/batches/types.ts @@ -71,10 +71,8 @@ export interface RetryPlan { // ── Provider catalog (D16 / D17) ───────────────────────────────────────────── export const BATCH_SUPPORTED_PROVIDERS = ["openai", "anthropic", "gemini"] as const; -export type BatchProvider = (typeof BATCH_SUPPORTED_PROVIDERS)[number]; // ── Re-exports ─────────────────────────────────────────────────────────────── export type { SupportedBatchEndpoint }; export { SUPPORTED_BATCH_ENDPOINTS }; - diff --git a/src/lib/cloudAgent/types.ts b/src/lib/cloudAgent/types.ts index 01d13fec22..90b3fa81ca 100644 --- a/src/lib/cloudAgent/types.ts +++ b/src/lib/cloudAgent/types.ts @@ -70,24 +70,6 @@ export const CloudAgentSourceSchema = z.object({ branch: z.string().optional(), }); -export const CloudAgentResultSchema = z.object({ - prUrl: z.string().url().optional(), - prNumber: z.number().int().positive().optional(), - commitMessage: z.string().optional(), - diffUrl: z.string().url().optional(), - summary: z.string().optional(), - duration: z.number().int().positive().optional(), - cost: z.number().positive().optional(), -}); - -export const CloudAgentActivitySchema = z.object({ - id: z.string(), - type: z.enum(["plan", "command", "code_change", "message", "error", "completion"]), - content: z.string(), - timestamp: z.string().datetime(), - metadata: z.record(z.string(), z.unknown()).optional(), -}); - export const CloudAgentTaskOptionsSchema = z.object({ autoCreatePr: z.boolean().optional(), planApprovalRequired: z.boolean().optional(), @@ -106,6 +88,3 @@ export const UpdateCloudAgentTaskSchema = z.object({ action: z.enum(["approve", "reject", "cancel", "message"]), message: z.string().optional(), }); - -export type CreateCloudAgentTaskInput = z.infer; -export type UpdateCloudAgentTaskInput = z.infer; diff --git a/src/lib/combos/builderOptions.ts b/src/lib/combos/builderOptions.ts index b343f17c90..b1dee3c1e9 100644 --- a/src/lib/combos/builderOptions.ts +++ b/src/lib/combos/builderOptions.ts @@ -315,6 +315,98 @@ function addModelOption( ); } +function buildModelOptions( + providerId: string, + builtInModels: RegistryModel[], + syncedModels: SyncedModelLike[], + customModels: CustomModelLike[] +): Map { + const modelMap = new Map(); + const fallbackModels = getCompatibleFallbackModels(providerId, builtInModels); + + for (const model of syncedModels) { + const resolved = getResolvedModelCapabilities({ + provider: providerId, + model: toStringOrNull(model.id), + }); + addModelOption(modelMap, providerId, { + id: toStringOrNull(model.id), + name: toStringOrNull(model.name), + source: "imported", + supportedEndpoints: toStringArray(model.supportedEndpoints), + contextLength: toNumberOrNull(model.inputTokenLimit) ?? resolved.contextWindow, + outputTokenLimit: toNumberOrNull(model.outputTokenLimit) ?? resolved.maxOutputTokens, + supportsThinking: + typeof model.supportsThinking === "boolean" + ? model.supportsThinking + : (resolved.supportsThinking ?? undefined), + }); + } + + for (const model of builtInModels) { + const resolved = getResolvedModelCapabilities({ + provider: providerId, + model: toStringOrNull(model.id), + }); + addModelOption(modelMap, providerId, { + id: toStringOrNull(model.id), + name: toStringOrNull(model.name), + source: "system", + contextLength: toNumberOrNull(model.contextLength) ?? resolved.contextWindow, + outputTokenLimit: resolved.maxOutputTokens, + supportsThinking: resolved.supportsThinking ?? undefined, + }); + } + + for (const model of customModels) { + if (model.isHidden === true) continue; + const source = ["api-sync", "auto-sync", "imported"].includes( + toStringOrNull(model.source)?.toLowerCase() || "" + ) + ? "imported" + : ("custom" as BuilderModelSource); + const resolved = getResolvedModelCapabilities({ + provider: providerId, + model: toStringOrNull(model.id), + }); + addModelOption(modelMap, providerId, { + id: toStringOrNull(model.id), + name: toStringOrNull(model.name), + source, + supportedEndpoints: toStringArray(model.supportedEndpoints), + apiFormat: toStringOrNull(model.apiFormat), + contextLength: toNumberOrNull(model.inputTokenLimit) ?? resolved.contextWindow, + outputTokenLimit: toNumberOrNull(model.outputTokenLimit) ?? resolved.maxOutputTokens, + supportsThinking: + typeof model.supportsThinking === "boolean" + ? model.supportsThinking + : (resolved.supportsThinking ?? undefined), + }); + } + + if (Array.isArray(fallbackModels)) { + for (const model of fallbackModels) { + const resolved = getResolvedModelCapabilities({ + provider: providerId, + model: toStringOrNull(model.id), + }); + addModelOption(modelMap, providerId, { + id: toStringOrNull(model.id), + name: toStringOrNull(model.name), + source: "fallback", + contextLength: + typeof (model as { contextLength?: number }).contextLength === "number" + ? (model as { contextLength?: number }).contextLength || null + : resolved.contextWindow, + outputTokenLimit: resolved.maxOutputTokens, + supportsThinking: resolved.supportsThinking ?? undefined, + }); + } + } + + return modelMap; +} + function compareConnections( left: ComboBuilderConnectionOption, right: ComboBuilderConnectionOption @@ -398,7 +490,6 @@ export async function getComboBuilderOptions(): Promise(); const builtInModels = getModelsByProviderId(providerId); const syncedModels = normalizeSyncedModels( (syncedModelsMap as Record)[providerId] @@ -406,92 +497,17 @@ export async function getComboBuilderOptions(): Promise)[providerId] ); - const fallbackModels = getCompatibleFallbackModels(providerId, builtInModels); const acceptsArbitraryModel = Boolean((AI_PROVIDERS[providerId] as JsonRecord | undefined)?.passthroughModels) || isOpenAICompatibleProvider(providerId) || isAnthropicCompatibleProvider(providerId) || isClaudeCodeCompatibleProvider(providerId); - - for (const model of syncedModels) { - const resolved = getResolvedModelCapabilities({ - provider: providerId, - model: toStringOrNull(model.id), - }); - addModelOption(modelMap, providerId, { - id: toStringOrNull(model.id), - name: toStringOrNull(model.name), - source: "imported", - supportedEndpoints: toStringArray(model.supportedEndpoints), - contextLength: toNumberOrNull(model.inputTokenLimit) ?? resolved.contextWindow, - outputTokenLimit: toNumberOrNull(model.outputTokenLimit) ?? resolved.maxOutputTokens, - supportsThinking: - typeof model.supportsThinking === "boolean" - ? model.supportsThinking - : (resolved.supportsThinking ?? undefined), - }); - } - - for (const model of builtInModels as RegistryModel[]) { - const resolved = getResolvedModelCapabilities({ - provider: providerId, - model: toStringOrNull(model.id), - }); - addModelOption(modelMap, providerId, { - id: toStringOrNull(model.id), - name: toStringOrNull(model.name), - source: "system", - contextLength: toNumberOrNull(model.contextLength) ?? resolved.contextWindow, - outputTokenLimit: resolved.maxOutputTokens, - supportsThinking: resolved.supportsThinking ?? undefined, - }); - } - - for (const model of customModels) { - if (model.isHidden === true) continue; - const source = ["api-sync", "auto-sync", "imported"].includes( - toStringOrNull(model.source)?.toLowerCase() || "" - ) - ? "imported" - : ("custom" as BuilderModelSource); - const resolved = getResolvedModelCapabilities({ - provider: providerId, - model: toStringOrNull(model.id), - }); - addModelOption(modelMap, providerId, { - id: toStringOrNull(model.id), - name: toStringOrNull(model.name), - source, - supportedEndpoints: toStringArray(model.supportedEndpoints), - apiFormat: toStringOrNull(model.apiFormat), - contextLength: toNumberOrNull(model.inputTokenLimit) ?? resolved.contextWindow, - outputTokenLimit: toNumberOrNull(model.outputTokenLimit) ?? resolved.maxOutputTokens, - supportsThinking: - typeof model.supportsThinking === "boolean" - ? model.supportsThinking - : (resolved.supportsThinking ?? undefined), - }); - } - - if (Array.isArray(fallbackModels)) { - for (const model of fallbackModels) { - const resolved = getResolvedModelCapabilities({ - provider: providerId, - model: toStringOrNull(model.id), - }); - addModelOption(modelMap, providerId, { - id: toStringOrNull(model.id), - name: toStringOrNull(model.name), - source: "fallback", - contextLength: - typeof (model as { contextLength?: number }).contextLength === "number" - ? (model as { contextLength?: number }).contextLength || null - : resolved.contextWindow, - outputTokenLimit: resolved.maxOutputTokens, - supportsThinking: resolved.supportsThinking ?? undefined, - }); - } - } + const modelMap = buildModelOptions( + providerId, + builtInModels as RegistryModel[], + syncedModels, + customModels + ); const normalizedConnections = providerConnections .map((connection) => buildConnectionOption(connection)) @@ -536,7 +552,6 @@ export async function getComboBuilderOptions(): Promise(); const builtInModels = getModelsByProviderId(providerId); const syncedModels = normalizeSyncedModels( (syncedModelsMap as Record)[providerId] @@ -544,92 +559,17 @@ export async function getComboBuilderOptions(): Promise)[providerId] ); - const fallbackModels = getCompatibleFallbackModels(providerId, builtInModels); const acceptsArbitraryModel = Boolean((AI_PROVIDERS[providerId] as JsonRecord | undefined)?.passthroughModels) || isOpenAICompatibleProvider(providerId) || isAnthropicCompatibleProvider(providerId) || isClaudeCodeCompatibleProvider(providerId); - - for (const model of syncedModels) { - const resolved = getResolvedModelCapabilities({ - provider: providerId, - model: toStringOrNull(model.id), - }); - addModelOption(modelMap, providerId, { - id: toStringOrNull(model.id), - name: toStringOrNull(model.name), - source: "imported", - supportedEndpoints: toStringArray(model.supportedEndpoints), - contextLength: toNumberOrNull(model.inputTokenLimit) ?? resolved.contextWindow, - outputTokenLimit: toNumberOrNull(model.outputTokenLimit) ?? resolved.maxOutputTokens, - supportsThinking: - typeof model.supportsThinking === "boolean" - ? model.supportsThinking - : (resolved.supportsThinking ?? undefined), - }); - } - - for (const model of builtInModels as RegistryModel[]) { - const resolved = getResolvedModelCapabilities({ - provider: providerId, - model: toStringOrNull(model.id), - }); - addModelOption(modelMap, providerId, { - id: toStringOrNull(model.id), - name: toStringOrNull(model.name), - source: "system", - contextLength: toNumberOrNull(model.contextLength) ?? resolved.contextWindow, - outputTokenLimit: resolved.maxOutputTokens, - supportsThinking: resolved.supportsThinking ?? undefined, - }); - } - - for (const model of customModels) { - if (model.isHidden === true) continue; - const source = ["api-sync", "auto-sync", "imported"].includes( - toStringOrNull(model.source)?.toLowerCase() || "" - ) - ? "imported" - : ("custom" as BuilderModelSource); - const resolved = getResolvedModelCapabilities({ - provider: providerId, - model: toStringOrNull(model.id), - }); - addModelOption(modelMap, providerId, { - id: toStringOrNull(model.id), - name: toStringOrNull(model.name), - source, - supportedEndpoints: toStringArray(model.supportedEndpoints), - apiFormat: toStringOrNull(model.apiFormat), - contextLength: toNumberOrNull(model.inputTokenLimit) ?? resolved.contextWindow, - outputTokenLimit: toNumberOrNull(model.outputTokenLimit) ?? resolved.maxOutputTokens, - supportsThinking: - typeof model.supportsThinking === "boolean" - ? model.supportsThinking - : (resolved.supportsThinking ?? undefined), - }); - } - - if (Array.isArray(fallbackModels)) { - for (const model of fallbackModels) { - const resolved = getResolvedModelCapabilities({ - provider: providerId, - model: toStringOrNull(model.id), - }); - addModelOption(modelMap, providerId, { - id: toStringOrNull(model.id), - name: toStringOrNull(model.name), - source: "fallback", - contextLength: - typeof (model as { contextLength?: number }).contextLength === "number" - ? (model as { contextLength?: number }).contextLength || null - : resolved.contextWindow, - outputTokenLimit: resolved.maxOutputTokens, - supportsThinking: resolved.supportsThinking ?? undefined, - }); - } - } + const modelMap = buildModelOptions( + providerId, + builtInModels as RegistryModel[], + syncedModels, + customModels + ); // #2901: no-auth providers must route under their alias (e.g. "oc"), not // their id — "opencode/" misroutes to the opencode-zen api-key tier diff --git a/src/lib/copilot/codegraphKnowledge.ts b/src/lib/copilot/codegraphKnowledge.ts index 31e0d23bfa..36919fbac9 100644 --- a/src/lib/copilot/codegraphKnowledge.ts +++ b/src/lib/copilot/codegraphKnowledge.ts @@ -31,27 +31,6 @@ export interface CodeGraphNode { visibility?: string; } -export interface CodeGraphEdge { - id: number; - source: string; - target: string; - kind: string; - line?: number; - metadata?: Record; -} - -export interface CodeGraphFile { - path: string; - language: string; - nodeCount: number; - modifiedAt: number; -} - -export interface CodeGraphSearchResult { - nodes: CodeGraphNode[]; - total: number; -} - // --------------------------------------------------------------------------- // Database access (lazy loaded) // --------------------------------------------------------------------------- @@ -231,30 +210,6 @@ export function listFiles(language?: string, limit = 50): CodeGraphQueryResult { return queryDb(`SELECT * FROM files ORDER BY path LIMIT ?`, [limit]); } -/** - * Get impact analysis: find symbols that depend on a given symbol (transitively). - */ -export function getImpactAnalysis(symbolName: string, depth = 1): CodeGraphQueryResult { - if (depth <= 0) - return { success: false, data: null, error: "Depth must be >= 1", engine: "none" }; - - // Direct callers (depth 1) - const directCallers = findCallers(symbolName); - if (depth === 1) return directCallers; - - // For depth > 1, we'd need recursive CTE or multiple queries. - // For now, just return direct callers with a note. - const result = directCallers; - return { - ...result, - data: (result.data as Record[])?.map((r) => ({ - ...r, - _depth: 1, - _note: `Depth > 1 requires multiple queries. Use searchSymbols() + findCallers() iteratively for deeper analysis.`, - })), - }; -} - /** * Check if CodeGraph DB is available. */ diff --git a/src/lib/db/adapters/driverFactory.ts b/src/lib/db/adapters/driverFactory.ts index ae342244e2..4975714e02 100644 --- a/src/lib/db/adapters/driverFactory.ts +++ b/src/lib/db/adapters/driverFactory.ts @@ -1,8 +1,10 @@ -// src/lib/db/adapters/driverFactory.ts -import fs from "node:fs"; import { createRequire } from "node:module"; import { createBetterSqliteAdapter } from "./betterSqliteAdapter"; -import type { SqliteAdapter, PreparedStatement, RunResult } from "./types"; +import { + createNodeSqliteAdapterFromDatabase, + type NodeSqliteDatabaseLike, +} from "./nodeSqliteShared"; +import type { SqliteAdapter } from "./types"; const _require = createRequire(import.meta.url); @@ -17,138 +19,6 @@ function getSqlJsCache(): Map { return globalThis.__omnirouteSqlJsAdapters; } -function buildNodeAdapterSync( - db: { - prepare(sql: string): { - run(...p: unknown[]): { changes: number | bigint; lastInsertRowid: number | bigint }; - get(...p: unknown[]): unknown; - all(...p: unknown[]): unknown[]; - }; - exec(sql: string): void; - close(): void; - }, - filePath: string -): SqliteAdapter { - let _isOpen = true; - const MAX_STMT_CACHE_SIZE = 200; - interface CachedStatement { - stmt: ReturnType; - sql: string; - } - const stmtCache = new Map(); - - function getCached(sql: string) { - let entry = stmtCache.get(sql); - if (entry) { - stmtCache.delete(sql); - stmtCache.set(sql, entry); - } else { - const stmt = db.prepare(sql); - if (stmtCache.size >= MAX_STMT_CACHE_SIZE) { - const oldestKey = stmtCache.keys().next().value; - if (oldestKey !== undefined) { - const oldest = stmtCache.get(oldestKey); - if (oldest?.stmt && "finalize" in oldest.stmt) { - try { (oldest.stmt as any).finalize(); } catch {} - } - stmtCache.delete(oldestKey); - } - } - entry = { stmt, sql }; - stmtCache.set(sql, entry); - } - return entry.stmt; - } - - function runSp(fn: (...args: unknown[]) => T, ...args: unknown[]): T { - const sp = `sp_${Math.random().toString(36).slice(2)}`; - db.exec(`SAVEPOINT "${sp}"`); - try { - const r = fn(...args); - db.exec(`RELEASE "${sp}"`); - return r; - } catch (e) { - try { - db.exec(`ROLLBACK TO "${sp}"`); - db.exec(`RELEASE "${sp}"`); - } catch {} - throw e; - } - } - - return { - driver: "node:sqlite", - get open() { - return _isOpen; - }, - get name() { - return filePath; - }, - prepare(sql: string): PreparedStatement { - const stmt = getCached(sql); - return { - run(...params: unknown[]): RunResult { - const r = stmt.run(...params); - return { - changes: Number(r.changes ?? 0), - lastInsertRowid: Number(r.lastInsertRowid ?? 0), - }; - }, - get(...params: unknown[]): unknown { - return stmt.get(...params); - }, - all(...params: unknown[]): unknown[] { - return stmt.all(...params); - }, - }; - }, - exec(sql: string): void { - db.exec(sql); - }, - pragma(pragmaStr: string, options?: { simple?: boolean }): unknown { - if (options?.simple) { - const row = db.prepare(`PRAGMA ${pragmaStr}`).get() as Record | undefined; - if (!row) return null; - return Object.values(row)[0] ?? null; - } - return db.prepare(`PRAGMA ${pragmaStr}`).all(); - }, - transaction(fn: (...args: unknown[]) => T): (...args: unknown[]) => T { - return (...args: unknown[]) => runSp(fn, ...args); - }, - immediate(fn: () => void): void { - runSp(() => fn()); - }, - async backup(destination: string): Promise { - try { - db.exec("PRAGMA wal_checkpoint(TRUNCATE)"); - } catch {} - fs.copyFileSync(filePath, destination); - }, - checkpoint(mode = "TRUNCATE"): void { - try { - db.exec(`PRAGMA wal_checkpoint(${mode})`); - } catch {} - }, - close(): void { - try { - for (const entry of stmtCache.values()) { - if (entry.stmt && "finalize" in entry.stmt) { - try { (entry.stmt as any).finalize(); } catch {} - } - } - stmtCache.clear(); - db.close(); - } finally { - _isOpen = false; - } - }, - get raw() { - return db; - }, - }; -} - /** Tenta abrir com better-sqlite3 e node:sqlite sincronamente. Retorna null se ambos falharem. */ export function tryOpenSync( filePath: string, @@ -173,10 +43,10 @@ export function tryOpenSync( if (maj > 22 || (maj === 22 && min >= 5)) { try { const { DatabaseSync } = _require("node:sqlite") as { - DatabaseSync: new (p: string) => Parameters[0]; + DatabaseSync: new (p: string) => NodeSqliteDatabaseLike; }; const db = new DatabaseSync(filePath); - return buildNodeAdapterSync(db, filePath); + return createNodeSqliteAdapterFromDatabase(db, filePath); } catch { // continua } diff --git a/src/lib/db/adapters/nodeSqliteAdapter.ts b/src/lib/db/adapters/nodeSqliteAdapter.ts index 21a1e2b578..73c3aeee60 100644 --- a/src/lib/db/adapters/nodeSqliteAdapter.ts +++ b/src/lib/db/adapters/nodeSqliteAdapter.ts @@ -1,6 +1,8 @@ -// src/lib/db/adapters/nodeSqliteAdapter.ts -import fs from "node:fs"; -import type { SqliteAdapter, PreparedStatement, RunResult } from "./types"; +import type { SqliteAdapter } from "./types"; +import { + createNodeSqliteAdapterFromDatabase, + type NodeSqliteDatabaseLike, +} from "./nodeSqliteShared"; const CHECKPOINT_INTERVAL_MS = 60_000; @@ -21,65 +23,11 @@ export async function createNodeSqliteAdapter(filePath: string): Promise { - prepare(sql: string): { - run(...p: unknown[]): { changes: number | bigint; lastInsertRowid: number | bigint }; - get(...p: unknown[]): unknown; - all(...p: unknown[]): unknown[]; - }; - exec(sql: string): void; - close(): void; - }; + DatabaseSync: new (path: string) => NodeSqliteDatabaseLike; }; const db = new DatabaseSync(filePath); - const MAX_STMT_CACHE_SIZE = 200; - interface CachedStatement { - stmt: ReturnType; - sql: string; - } - const stmtCache = new Map(); - - function getCached(sql: string) { - let entry = stmtCache.get(sql); - if (entry) { - stmtCache.delete(sql); - stmtCache.set(sql, entry); - } else { - const stmt = db.prepare(sql); - if (stmtCache.size >= MAX_STMT_CACHE_SIZE) { - const oldestKey = stmtCache.keys().next().value; - if (oldestKey !== undefined) { - const oldest = stmtCache.get(oldestKey); - if (oldest?.stmt && "finalize" in oldest.stmt) { - try { (oldest.stmt as any).finalize(); } catch {} - } - stmtCache.delete(oldestKey); - } - } - entry = { stmt, sql }; - stmtCache.set(sql, entry); - } - return entry.stmt; - } - - function runSavepoint(fn: (...args: unknown[]) => T, ...args: unknown[]): T { - const sp = `sp_${Math.random().toString(36).slice(2)}`; - db.exec(`SAVEPOINT "${sp}"`); - try { - const result = fn(...args); - db.exec(`RELEASE "${sp}"`); - return result; - } catch (err) { - try { - db.exec(`ROLLBACK TO "${sp}"`); - db.exec(`RELEASE "${sp}"`); - } catch {} - throw err; - } - } - const checkpointTimer = setInterval(() => { try { db.exec("PRAGMA wal_checkpoint(TRUNCATE)"); @@ -87,108 +35,26 @@ export async function createNodeSqliteAdapter(filePath: string): Promise { + adapter.close(); + }); process.once("SIGINT", () => { - gracefulClose(); + adapter.close(); process.exit(0); }); process.once("SIGTERM", () => { - gracefulClose(); + adapter.close(); process.exit(0); }); - return { - driver: "node:sqlite", - - get open() { - return _isOpen; - }, - - get name() { - return filePath; - }, - - prepare(sql: string): PreparedStatement { - const stmt = getCached(sql); - return { - run(...params: unknown[]): RunResult { - const r = stmt.run(...params); - return { - changes: Number(r.changes ?? 0), - lastInsertRowid: Number(r.lastInsertRowid ?? 0), - }; - }, - get(...params: unknown[]): unknown { - return stmt.get(...params); - }, - all(...params: unknown[]): unknown[] { - return stmt.all(...params); - }, - }; - }, - - exec(sql: string): void { - db.exec(sql); - }, - - pragma(pragmaStr: string, options?: { simple?: boolean }): unknown { - const sql = `PRAGMA ${pragmaStr}`; - if (options?.simple) { - const row = db.prepare(sql).get() as Record | undefined; - if (!row) return null; - return Object.values(row)[0] ?? null; - } - return db.prepare(sql).all(); - }, - - transaction(fn: (...args: unknown[]) => T): (...args: unknown[]) => T { - return (...args: unknown[]) => runSavepoint(fn, ...args); - }, - - immediate(fn: () => void): void { - runSavepoint(() => fn()); - }, - - async backup(destination: string): Promise { - try { - db.exec("PRAGMA wal_checkpoint(TRUNCATE)"); - } catch {} - fs.copyFileSync(filePath, destination); - }, - - checkpoint(mode = "TRUNCATE"): void { - try { - db.exec(`PRAGMA wal_checkpoint(${mode})`); - } catch {} - }, - - close(): void { - gracefulClose(); - }, - - get raw() { - return db; - }, - }; + return adapter; } diff --git a/src/lib/db/adapters/nodeSqliteShared.ts b/src/lib/db/adapters/nodeSqliteShared.ts new file mode 100644 index 0000000000..e301318f15 --- /dev/null +++ b/src/lib/db/adapters/nodeSqliteShared.ts @@ -0,0 +1,148 @@ +import fs from "node:fs"; +import type { PreparedStatement, RunResult, SqliteAdapter } from "./types"; + +export interface NodeSqliteDatabaseLike { + prepare(sql: string): { + run(...p: unknown[]): { changes: number | bigint; lastInsertRowid: number | bigint }; + get(...p: unknown[]): unknown; + all(...p: unknown[]): unknown[]; + }; + exec(sql: string): void; + close(): void; +} + +const MAX_STMT_CACHE_SIZE = 200; + +export function createNodeSqliteAdapterFromDatabase( + db: NodeSqliteDatabaseLike, + filePath: string, + onClose?: () => void +): SqliteAdapter { + let _isOpen = true; + type NodeSqliteStatement = ReturnType; + interface CachedStatement { + stmt: NodeSqliteStatement; + } + const stmtCache = new Map(); + + function finalizeStatement(stmt: NodeSqliteStatement | undefined) { + if (stmt && "finalize" in stmt) { + try { + (stmt as NodeSqliteStatement & { finalize: () => void }).finalize(); + } catch {} + } + } + + function getCached(sql: string) { + let entry = stmtCache.get(sql); + if (entry) { + stmtCache.delete(sql); + stmtCache.set(sql, entry); + } else { + const stmt = db.prepare(sql); + if (stmtCache.size >= MAX_STMT_CACHE_SIZE) { + const oldestKey = stmtCache.keys().next().value; + if (oldestKey !== undefined) { + finalizeStatement(stmtCache.get(oldestKey)?.stmt); + stmtCache.delete(oldestKey); + } + } + entry = { stmt }; + stmtCache.set(sql, entry); + } + return entry.stmt; + } + + function runSavepoint(fn: (...args: unknown[]) => T, ...args: unknown[]): T { + const sp = `sp_${Math.random().toString(36).slice(2)}`; + db.exec(`SAVEPOINT "${sp}"`); + try { + const result = fn(...args); + db.exec(`RELEASE "${sp}"`); + return result; + } catch (err) { + try { + db.exec(`ROLLBACK TO "${sp}"`); + db.exec(`RELEASE "${sp}"`); + } catch {} + throw err; + } + } + + function close() { + try { + onClose?.(); + } catch {} + try { + for (const entry of stmtCache.values()) { + finalizeStatement(entry.stmt); + } + stmtCache.clear(); + } catch {} + try { + db.close(); + } catch {} + _isOpen = false; + } + + return { + driver: "node:sqlite", + get open() { + return _isOpen; + }, + get name() { + return filePath; + }, + prepare(sql: string): PreparedStatement { + const stmt = getCached(sql); + return { + run(...params: unknown[]): RunResult { + const r = stmt.run(...params); + return { + changes: Number(r.changes ?? 0), + lastInsertRowid: Number(r.lastInsertRowid ?? 0), + }; + }, + get(...params: unknown[]): unknown { + return stmt.get(...params); + }, + all(...params: unknown[]): unknown[] { + return stmt.all(...params); + }, + }; + }, + exec(sql: string): void { + db.exec(sql); + }, + pragma(pragmaStr: string, options?: { simple?: boolean }): unknown { + const sql = `PRAGMA ${pragmaStr}`; + if (options?.simple) { + const row = db.prepare(sql).get() as Record | undefined; + if (!row) return null; + return Object.values(row)[0] ?? null; + } + return db.prepare(sql).all(); + }, + transaction(fn: (...args: unknown[]) => T): (...args: unknown[]) => T { + return (...args: unknown[]) => runSavepoint(fn, ...args); + }, + immediate(fn: () => void): void { + runSavepoint(() => fn()); + }, + async backup(destination: string): Promise { + try { + db.exec("PRAGMA wal_checkpoint(TRUNCATE)"); + } catch {} + fs.copyFileSync(filePath, destination); + }, + checkpoint(mode = "TRUNCATE"): void { + try { + db.exec(`PRAGMA wal_checkpoint(${mode})`); + } catch {} + }, + close, + get raw() { + return db; + }, + }; +} diff --git a/src/lib/db/webhookDeliveries.ts b/src/lib/db/webhookDeliveries.ts index ba34237ae7..17ecb912fe 100644 --- a/src/lib/db/webhookDeliveries.ts +++ b/src/lib/db/webhookDeliveries.ts @@ -75,12 +75,3 @@ export function getDeliveries(webhookId: string, limit: number): WebhookDelivery ) .all(webhookId, limit) as WebhookDeliverySafe[]; } - -/** Fetch one delivery including `payload_snapshot` — used for opt-in detail view. */ -export function getDeliveryDetail(webhookId: string, deliveryId: number): WebhookDelivery | null { - const db = getDbInstance(); - const row = db - .prepare(`SELECT * FROM webhook_deliveries WHERE webhook_id = ? AND id = ? LIMIT 1`) - .get(webhookId, deliveryId); - return (row as WebhookDelivery | undefined) ?? null; -} diff --git a/src/lib/gamification/streaks.ts b/src/lib/gamification/streaks.ts index d95f6abde8..b4973ff93e 100644 --- a/src/lib/gamification/streaks.ts +++ b/src/lib/gamification/streaks.ts @@ -162,33 +162,3 @@ export async function updateStreak(apiKeyId: string): Promise { return newStreak; } - -/** - * Check if a streak is still active (last activity within the last 2 days). - * Does not modify any data. - * - * @param apiKeyId - The API key identifier - * @returns true if the streak is still alive - */ -export async function isStreakActive(apiKeyId: string): Promise { - const streak = await getStreak(apiKeyId); - if (!streak.lastActiveDate) return false; - - const last = new Date(streak.lastActiveDate + "T00:00:00Z").getTime(); - const now = Date.now(); - const daysSince = Math.floor((now - last) / MS_PER_DAY); - - return daysSince <= 1; // Today or yesterday -} - -/** - * Reset streak data for an API key (admin/testing use). - * - * @param apiKeyId - The API key identifier - */ -export async function resetStreak(apiKeyId: string): Promise { - if (isBuildPhase || isCloud) return; - - const db = getDbInstance() as unknown as DbLike; - db.prepare("DELETE FROM key_value WHERE namespace = ? AND key = ?").run(NAMESPACE, apiKeyId); -} diff --git a/src/lib/headroom/process.ts b/src/lib/headroom/process.ts index b788bf6600..7e5ddeaeaf 100644 --- a/src/lib/headroom/process.ts +++ b/src/lib/headroom/process.ts @@ -89,9 +89,7 @@ function safePort(port: unknown): number { return Number.isFinite(n) && n > 0 && n < 65536 ? n : DEFAULT_PORT; } -export async function startHeadroomProxy( - opts: { port?: number } = {} -): Promise { +export async function startHeadroomProxy(opts: { port?: number } = {}): Promise { const binary = findHeadroomBinary(); if (!binary) { throw new HeadroomError("Headroom CLI not installed", "NOT_INSTALLED"); @@ -125,10 +123,7 @@ export async function startHeadroomProxy( if (isPidAlive(child.pid!)) resolve(); else reject( - new HeadroomError( - "headroom proxy exited during startup — see proxy.log", - "EARLY_EXIT" - ) + new HeadroomError("headroom proxy exited during startup — see proxy.log", "EARLY_EXIT") ); }, STARTUP_TIMEOUT_MS); @@ -180,14 +175,3 @@ export function stopHeadroomProxy(): StopResult { throw new HeadroomError(`Failed to stop headroom proxy: ${msg}`, "STOP_FAILED"); } } - -export function getHeadroomLogTail(maxLines = 200): string { - try { - if (!fs.existsSync(LOG_FILE)) return ""; - const content = fs.readFileSync(LOG_FILE, "utf8"); - const lines = content.split(/\r?\n/).filter(Boolean); - return lines.slice(-maxLines).join("\n"); - } catch { - return ""; - } -} diff --git a/src/lib/memory/__tests__/schemas.test.ts b/src/lib/memory/__tests__/schemas.test.ts index ca0d908df7..167e9df17e 100644 --- a/src/lib/memory/__tests__/schemas.test.ts +++ b/src/lib/memory/__tests__/schemas.test.ts @@ -24,6 +24,12 @@ describe("Memory Schemas", () => { metadata: { updatedAt: new Date() }, }; + test("exports runtime schemas", () => { + expect(typeof MemoryConfigSchema.safeParse).toBe("function"); + expect(typeof MemoryCreateInputSchema.safeParse).toBe("function"); + expect(typeof MemoryUpdateInputSchema.safeParse).toBe("function"); + }); + test("MemoryConfigSchema validation", () => { expect(MemoryConfigSchema.parse(validConfig)).toBeDefined(); const invalidConfig = { ...validConfig, maxTokens: -1 }; @@ -38,7 +44,7 @@ describe("Memory Schemas", () => { test("MemoryUpdateInputSchema validation", () => { expect(MemoryUpdateInputSchema.parse(validUpdateInput)).toBeDefined(); - const invalidUpdate = { key: "test" }; + const invalidUpdate = { key: "" }; expect(() => MemoryUpdateInputSchema.parse(invalidUpdate)).toThrow(); }); }); diff --git a/src/lib/memory/schemas.ts b/src/lib/memory/schemas.ts index e3d84c22a2..0b31284022 100644 --- a/src/lib/memory/schemas.ts +++ b/src/lib/memory/schemas.ts @@ -37,10 +37,3 @@ export const MemoryUpdateInputSchema = z metadata: z.record(z.string(), z.unknown()).optional(), }) .strict(); - -/** - * Exported schema types for TypeScript references - */ -export type MemoryConfig = z.infer; -export type MemoryCreateInput = z.infer; -export type MemoryUpdateInput = z.infer; diff --git a/src/lib/notion/api.ts b/src/lib/notion/api.ts index 5e52b0cc2d..e32074b711 100644 --- a/src/lib/notion/api.ts +++ b/src/lib/notion/api.ts @@ -245,5 +245,3 @@ export function createNotionClient(apiKey: string) { return client; } - -export type NotionClient = ReturnType; diff --git a/src/lib/oauth/utils/claudeAuthZipExtract.ts b/src/lib/oauth/utils/claudeAuthZipExtract.ts index 8133851014..3de5ff1dfe 100644 --- a/src/lib/oauth/utils/claudeAuthZipExtract.ts +++ b/src/lib/oauth/utils/claudeAuthZipExtract.ts @@ -1,89 +1,5 @@ -import path from "path"; -import { unzipSync, type Unzipped } from "fflate"; - -export interface ExtractedZipFile { - name: string; - content: string; -} - -export interface ExtractZipOptions { - maxFiles?: number; - maxFileSizeBytes?: number; - maxTotalSizeBytes?: number; -} - -const DEFAULT_MAX_FILES = 50; -const DEFAULT_MAX_FILE_SIZE = 256 * 1024; -const DEFAULT_MAX_TOTAL = 10 * 1024 * 1024; - -function isSafeEntryName(name: string): boolean { - if (!name.toLowerCase().endsWith(".json")) return false; - if (name.includes("..")) return false; - if (path.isAbsolute(name)) return false; - if (/[\r\n\0]/.test(name)) return false; - return true; -} - -export function extractClaudeAuthZip( - zipBuffer: Buffer, - options: ExtractZipOptions = {} -): ExtractedZipFile[] { - const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES; - const maxFileSize = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE; - const maxTotal = options.maxTotalSizeBytes ?? DEFAULT_MAX_TOTAL; - - let unzipped: Unzipped; - try { - unzipped = unzipSync(new Uint8Array(zipBuffer)); - } catch { - throw new Error("Could not parse ZIP archive — file may be corrupt or not a valid ZIP"); - } - - const entries = Object.entries(unzipped).filter(([, data]) => data !== undefined); - const jsonEntries = entries.filter(([name]) => name.toLowerCase().endsWith(".json")); - - if (jsonEntries.length === 0) { - throw new Error("ZIP archive contains no .json files"); - } - - if (jsonEntries.length > maxFiles) { - throw new Error( - `ZIP archive contains ${jsonEntries.length} .json files — max allowed is ${maxFiles}` - ); - } - - let totalBytes = 0; - const result: ExtractedZipFile[] = []; - - for (const [entryName, data] of jsonEntries) { - const baseName = path.basename(entryName); - - if (!isSafeEntryName(baseName)) { - throw new Error( - `ZIP entry "${baseName}" has an unsafe filename (must be a .json file without path traversal)` - ); - } - - if (!isSafeEntryName(entryName)) { - throw new Error( - `ZIP entry path "${entryName}" is unsafe (no "..", absolute paths, or control characters allowed)` - ); - } - - if (data.byteLength > maxFileSize) { - throw new Error( - `ZIP entry "${baseName}" is ${data.byteLength} bytes — exceeds ${maxFileSize} byte limit per file` - ); - } - - totalBytes += data.byteLength; - if (totalBytes > maxTotal) { - throw new Error(`ZIP archive total uncompressed size exceeds ${maxTotal} byte limit`); - } - - const content = new TextDecoder("utf-8").decode(data); - result.push({ name: baseName, content }); - } - - return result; -} +export { + extractJsonZip as extractClaudeAuthZip, + type ExtractedZipFile, + type ExtractZipOptions, +} from "@/lib/oauth/utils/jsonZipExtract"; diff --git a/src/lib/oauth/utils/codexAuthImport.ts b/src/lib/oauth/utils/codexAuthImport.ts index 54c7a98d02..a27d97b126 100644 --- a/src/lib/oauth/utils/codexAuthImport.ts +++ b/src/lib/oauth/utils/codexAuthImport.ts @@ -56,18 +56,6 @@ function extractCodexAccountId( // ──── Public types ──────────────────────────────────────────────────────────── -export interface CodexAuthFileInput { - auth_mode?: unknown; - OPENAI_API_KEY?: unknown; - tokens?: { - id_token?: unknown; - access_token?: unknown; - refresh_token?: unknown; - account_id?: unknown; - }; - last_refresh?: unknown; -} - export interface ParsedCodexAuth { idToken: string; accessToken: string; diff --git a/src/lib/oauth/utils/codexAuthZipExtract.ts b/src/lib/oauth/utils/codexAuthZipExtract.ts index 108c78c746..c55286d3a0 100644 --- a/src/lib/oauth/utils/codexAuthZipExtract.ts +++ b/src/lib/oauth/utils/codexAuthZipExtract.ts @@ -1,89 +1,5 @@ -import path from "path"; -import { unzipSync, type Unzipped } from "fflate"; - -export interface ExtractedZipFile { - name: string; - content: string; -} - -export interface ExtractZipOptions { - maxFiles?: number; - maxFileSizeBytes?: number; - maxTotalSizeBytes?: number; -} - -const DEFAULT_MAX_FILES = 50; -const DEFAULT_MAX_FILE_SIZE = 256 * 1024; -const DEFAULT_MAX_TOTAL = 10 * 1024 * 1024; - -function isSafeEntryName(name: string): boolean { - if (!name.toLowerCase().endsWith(".json")) return false; - if (name.includes("..")) return false; - if (path.isAbsolute(name)) return false; - if (/[\r\n\0]/.test(name)) return false; - return true; -} - -export function extractCodexAuthZip( - zipBuffer: Buffer, - options: ExtractZipOptions = {} -): ExtractedZipFile[] { - const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES; - const maxFileSize = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE; - const maxTotal = options.maxTotalSizeBytes ?? DEFAULT_MAX_TOTAL; - - let unzipped: Unzipped; - try { - unzipped = unzipSync(new Uint8Array(zipBuffer)); - } catch { - throw new Error("Could not parse ZIP archive — file may be corrupt or not a valid ZIP"); - } - - const entries = Object.entries(unzipped).filter(([, data]) => data !== undefined); - const jsonEntries = entries.filter(([name]) => name.toLowerCase().endsWith(".json")); - - if (jsonEntries.length === 0) { - throw new Error("ZIP archive contains no .json files"); - } - - if (jsonEntries.length > maxFiles) { - throw new Error( - `ZIP archive contains ${jsonEntries.length} .json files — max allowed is ${maxFiles}` - ); - } - - let totalBytes = 0; - const result: ExtractedZipFile[] = []; - - for (const [entryName, data] of jsonEntries) { - const baseName = path.basename(entryName); - - if (!isSafeEntryName(baseName)) { - throw new Error( - `ZIP entry "${baseName}" has an unsafe filename (must be a .json file without path traversal)` - ); - } - - if (!isSafeEntryName(entryName)) { - throw new Error( - `ZIP entry path "${entryName}" is unsafe (no "..", absolute paths, or control characters allowed)` - ); - } - - if (data.byteLength > maxFileSize) { - throw new Error( - `ZIP entry "${baseName}" is ${data.byteLength} bytes — exceeds ${maxFileSize} byte limit per file` - ); - } - - totalBytes += data.byteLength; - if (totalBytes > maxTotal) { - throw new Error(`ZIP archive total uncompressed size exceeds ${maxTotal} byte limit`); - } - - const content = new TextDecoder("utf-8").decode(data); - result.push({ name: baseName, content }); - } - - return result; -} +export { + extractJsonZip as extractCodexAuthZip, + type ExtractedZipFile, + type ExtractZipOptions, +} from "@/lib/oauth/utils/jsonZipExtract"; diff --git a/src/lib/promptCache/index.ts b/src/lib/promptCache/index.ts index 2d1446f864..4fa4e6a03f 100644 --- a/src/lib/promptCache/index.ts +++ b/src/lib/promptCache/index.ts @@ -1 +1 @@ -export { analyzePrefix, shouldInjectCacheControl, generatePromptCacheKey } from "./prefixAnalyzer"; +export { analyzePrefix, generatePromptCacheKey } from "./prefixAnalyzer"; diff --git a/src/lib/promptCache/prefixAnalyzer.ts b/src/lib/promptCache/prefixAnalyzer.ts index 6f8e86e12e..b3aef53680 100644 --- a/src/lib/promptCache/prefixAnalyzer.ts +++ b/src/lib/promptCache/prefixAnalyzer.ts @@ -72,10 +72,6 @@ export function analyzePrefix(messages: Message[]): PrefixAnalysis { }; } -export function shouldInjectCacheControl(analysis: PrefixAnalysis, minTokens = 1024): boolean { - return analysis.prefixTokens >= minTokens && analysis.confidence >= 0.7; -} - export function generatePromptCacheKey(messages: Message[]): string { const analysis = analyzePrefix(messages); if (analysis.prefixHash) { diff --git a/src/lib/providers/modelListingCapability.ts b/src/lib/providers/modelListingCapability.ts new file mode 100644 index 0000000000..3291b8517b --- /dev/null +++ b/src/lib/providers/modelListingCapability.ts @@ -0,0 +1,26 @@ +// #5420 — Tool-only providers (web search / web fetch) do not expose a model +// listing; their "Import Models" button hits the `400 "does not support models +// listing"` route. The old `-search` suffix heuristic caught `brave-search` but +// missed tool-only providers whose id has no suffix (e.g. `firecrawl`, declared +// `serviceKinds: ["webFetch"]`). This pure helper decides, from the provider id +// plus its resolved serviceKinds, whether to hide model listing — without ever +// hiding an LLM or media provider that genuinely lists models. Leaf module: it +// imports nothing, so it cannot create an import cycle with the page. + +/** Service kinds that, on their own, mean the provider lists no models. */ +const TOOL_ONLY_SERVICE_KINDS = new Set(["webSearch", "webFetch"]); + +/** + * True when the provider is tool-only and therefore has no model listing: + * - its id ends in `-search` (legacy search providers), OR + * - it declares at least one serviceKind and EVERY declared kind is a tool-only + * kind (`webSearch` / `webFetch`) — i.e. no `llm` and no media/embedding kind. + * + * Returns false for an empty `kinds` (most LLM providers declare nothing) and for + * any provider that has `llm`/image/video/music/tts/stt/embedding. + */ +export function providerLacksModelListing(providerId: string, kinds: readonly string[]): boolean { + if (providerId.endsWith("-search")) return true; + if (kinds.length === 0) return false; + return kinds.every((kind) => TOOL_ONLY_SERVICE_KINDS.has(kind)); +} diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index 091cc51aa6..1afdf2d454 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -10,10 +10,7 @@ import { providerAllowsOptionalApiKey, WEB_COOKIE_PROVIDERS, } from "@/shared/constants/providers"; -import { - SAFE_OUTBOUND_FETCH_PRESETS, - safeOutboundFetch, -} from "@/shared/network/safeOutboundFetch"; +import { SAFE_OUTBOUND_FETCH_PRESETS, safeOutboundFetch } from "@/shared/network/safeOutboundFetch"; import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuard"; import { resolveNvidiaValidationModel } from "@/lib/providers/nvidiaValidationModel"; import { validateQoderCliPat } from "@omniroute/open-sse/services/qoderCli.ts"; @@ -26,16 +23,8 @@ import { addModelsSuffix, resolveBaseUrl, } from "./validation/urlHelpers"; -import { - STANDARD_USER_AGENT, - directHttpsRequest, - buildBearerHeaders, -} from "./validation/headers"; -import { - validationRead, - validationWrite, - toValidationErrorResult, -} from "./validation/transport"; +import { STANDARD_USER_AGENT, directHttpsRequest, buildBearerHeaders } from "./validation/headers"; +import { validationRead, validationWrite, toValidationErrorResult } from "./validation/transport"; import { validateDeepSeekWebProvider, validateQwenWebProvider, @@ -81,10 +70,7 @@ import { validateNousResearchProvider, validatePoeProvider, } from "./validation/audioMiscProviders"; -import { - validateSearchProvider, - SEARCH_VALIDATOR_CONFIGS, -} from "./validation/searchProviders"; +import { validateSearchProvider, SEARCH_VALIDATOR_CONFIGS } from "./validation/searchProviders"; import { validateClarifaiProvider, validateEmbeddingApiProvider, @@ -166,6 +152,38 @@ export async function validateWebCookieProvider({ } } +// #5422: Bytez key validation cannot use a chat probe. A Bytez account only serves models +// that have been added to its catalog, so even Bytez's own documented model ids return 404 +// ("Model does not exist or has yet to be added to the Bytez catalog") for a fresh/free key — +// the generic OpenAI-like chat probe misreads that 404 as "endpoint not supported". Validate +// against the model-independent, auth-only tasks endpoint instead (verified live): +// GET …/models/v2/list/tasks → 200 (valid key) | 401 { error: "Unauthorized" } (invalid). +// The pure status→result mapping is factored out so it is unit-testable without network. +export function bytezValidationResultFromStatus(status: number): { + valid: boolean; + error: string | null; +} { + if (status === 200) { + return { valid: true, error: null }; + } + if (status === 401 || status === 403) { + return { valid: false, error: "Invalid API key" }; + } + return { valid: false, error: `Validation failed: ${status}` }; +} + +export async function validateBytezProvider({ apiKey, providerSpecificData = {} }: any) { + try { + const res = await validationRead("https://api.bytez.com/models/v2/list/tasks", { + method: "GET", + headers: buildBearerHeaders(apiKey, providerSpecificData), + }); + return bytezValidationResultFromStatus(res.status); + } catch (error: unknown) { + return toValidationErrorResult(error); + } +} + export async function validateProviderApiKey({ provider, apiKey, providerSpecificData = {} }: any) { const requiresApiKey = !providerAllowsOptionalApiKey(provider); const isLocal = isLocalProvider(provider); @@ -282,6 +300,9 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi }, "command-code": validateCommandCodeProvider, huggingface: validateHuggingFaceProvider, + // #5422: auth-only probe — Bytez 404s on every chat model until the account adds it to + // its catalog, so the generic chat probe can't validate a fresh key. + bytez: validateBytezProvider, deepgram: validateDeepgramProvider, assemblyai: validateAssemblyAIProvider, "fal-ai": ({ apiKey, providerSpecificData }: any) => @@ -421,7 +442,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi method: "POST", headers: buildBearerHeaders(apiKey, providerSpecificData), body: JSON.stringify({ - model: "longcat", + model: "LongCat-2.0", messages: [{ role: "user", content: "test" }], max_tokens: 1, }), diff --git a/src/lib/providers/validation/cozeError.ts b/src/lib/providers/validation/cozeError.ts new file mode 100644 index 0000000000..adb3325df7 --- /dev/null +++ b/src/lib/providers/validation/cozeError.ts @@ -0,0 +1,56 @@ +// #5426 — Coze surfaces key-validation failures as a JSON envelope shaped like +// `{ "code": 4100, "msg": "...", "logId": "...", "from": "bot-api" }`. Left +// untranslated, that raw envelope (logId included) leaks verbatim into the +// connection-validation UI. This pure helper recognizes the Coze shape and +// composes a friendly, leak-free message so callers can surface it instead of +// the raw body. Kept in its own leaf module (no network deps) so it is unit- +// testable in isolation. + +function asRecord(value: unknown): Record | null { + if (typeof value === "string") { + try { + const parsed = JSON.parse(value); + return parsed && typeof parsed === "object" && !Array.isArray(parsed) + ? (parsed as Record) + : null; + } catch { + return null; + } + } + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : null; +} + +/** + * When `body` looks like a Coze error envelope (a string `msg`, `from === "bot-api"`, + * or a `logId`), return a friendly one-line message composed from `msg`/`code` + * (e.g. `Coze rejected the key: (code )`). Returns `null` for anything + * that is not a Coze envelope — including normal OpenAI-style errors, non-objects, + * empty objects, and non-JSON strings — so non-Coze callers fall through unchanged. + * Never echoes the raw `logId` or the whole body. + */ +export function extractCozeValidationError(body: unknown): string | null { + const record = asRecord(body); + if (!record) return null; + + const msg = typeof record.msg === "string" ? record.msg.trim() : ""; + const from = typeof record.from === "string" ? record.from : ""; + const logId = typeof record.logId === "string" ? record.logId.trim() : ""; + + const looksLikeCoze = msg !== "" || from === "bot-api" || logId !== ""; + if (!looksLikeCoze) return null; + + const code = record.code; + const codeStr = + typeof code === "number" + ? String(code) + : typeof code === "string" && code.trim() !== "" + ? code.trim() + : ""; + + const detail = msg || "the API key was rejected"; + return codeStr + ? `Coze rejected the key: ${detail} (code ${codeStr})` + : `Coze rejected the key: ${detail}`; +} diff --git a/src/lib/providers/validation/openaiFormat.ts b/src/lib/providers/validation/openaiFormat.ts index 0dba98a56e..c9cc575ef8 100644 --- a/src/lib/providers/validation/openaiFormat.ts +++ b/src/lib/providers/validation/openaiFormat.ts @@ -12,6 +12,7 @@ import { addModelsSuffix, normalizeBaseUrl, resolveChatUrl } from "./urlHelpers" import { applyCustomUserAgent, buildBearerHeaders } from "./headers"; import { toValidationErrorResult, validationRead, validationWrite } from "./transport"; import { validateDirectChatProvider } from "./directChatProbe"; +import { extractCozeValidationError } from "./cozeError"; export async function validateBedrockProvider({ apiKey, providerSpecificData = {} }: any) { if (!apiKey) { @@ -143,6 +144,20 @@ export async function validateOpenAILikeProvider({ return { valid: true, error: null }; } + // #5426: Coze answers the chat probe with a JSON envelope ({ code, msg, + // logId, from }) on a bad key. Translate it into a friendly message so the + // raw envelope (logId included) never leaks into the connection UI. Scoped + // to provider === "coze" so a non-Coze error body that happens to carry a + // `msg` field is never mislabeled, and other providers' response bodies are + // never consumed here — they fall through to the canned handling below. + if (provider === "coze") { + const chatErrorBody = await chatRes.text().catch(() => ""); + const cozeError = extractCozeValidationError(chatErrorBody); + if (cozeError) { + return { valid: false, error: cozeError }; + } + } + if (chatRes.status === 401 || chatRes.status === 403) { return { valid: false, error: "Invalid API key" }; } diff --git a/src/lib/services/installers/ninerouter.ts b/src/lib/services/installers/ninerouter.ts index 44ef36eb5d..7535fa1715 100644 --- a/src/lib/services/installers/ninerouter.ts +++ b/src/lib/services/installers/ninerouter.ts @@ -77,16 +77,10 @@ export async function install(version = "latest"): Promise { } await runNpm( - [ - "install", - `${NINEROUTER_PACKAGE}@${version}`, - "--omit=dev", - "--no-audit", - "--no-fund", - "--prefix", - NINEROUTER_INSTALL_DIR, - ], - { cwd: NINEROUTER_INSTALL_DIR } + ["install", `${NINEROUTER_PACKAGE}@${version}`, "--omit=dev", "--no-audit", "--no-fund"], + // `--prefix` is passed via `prefix` (→ npm_config_prefix env) instead of an + // argv path so an install dir with spaces survives the Windows shell (#5379). + { cwd: NINEROUTER_INSTALL_DIR, prefix: NINEROUTER_INSTALL_DIR } ); const installedVersion = await getInstalledVersion(); diff --git a/src/lib/services/installers/utils.ts b/src/lib/services/installers/utils.ts index d69efc10be..a8efe66e48 100644 --- a/src/lib/services/installers/utils.ts +++ b/src/lib/services/installers/utils.ts @@ -73,10 +73,68 @@ function classifyError( return new InstallError(raw, `Falha na instalação: ${raw}`, 500); } -/** Runs npm with the given args array. Never uses shell interpolation. */ +/** + * Validates a user-supplied service version (npm dist-tag or semver). Constrained + * to letters, digits and `. _ + -`, with a leading alphanumeric, so the value can + * never carry shell metacharacters once `runNpm` runs under a shell on Windows + * (see `buildNpmExecOptions`). Accepts `latest`, `next`, `1.2.3`, `1.2.3-beta.1`, + * `1.2.3+build.5`; rejects `latest && calc`, `$(id)`, spaces, leading `-`, etc. + */ +export const SERVICE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/; + +export interface NpmExecOptions { + cwd?: string; + timeout: number; + env: NodeJS.ProcessEnv; + maxBuffer: number; + shell?: boolean; +} + +/** + * Builds the `execFile` options for {@link runNpm}. + * + * On Windows, npm is `npm.cmd` (a batch wrapper). Node 24 refuses to `execFile` + * a `.cmd` without a shell (nodejs/node#52554 — manifests as `spawn EINVAL`, see + * issue #5379), so we enable `shell` on win32 only. + * + * Enabling the shell means the shell — not `execFile` — splits the command line, + * so NO runtime value may be interpolated into argv (Hard Rule #13). The install + * prefix (a DATA_DIR path that can legitimately contain spaces, e.g. + * `C:\Users\John Doe\.omniroute\…`) is therefore exported as the + * `npm_config_prefix` environment variable — npm's documented env form of + * `--prefix` — never as an argv entry. With the prefix moved to the environment + * and the version constrained by {@link SERVICE_VERSION_PATTERN}, every remaining + * argv entry is a static, metacharacter-free flag. + */ +export function buildNpmExecOptions( + platform: NodeJS.Platform, + options: { cwd?: string; timeoutMs: number; prefix?: string } +): NpmExecOptions { + const env: NodeJS.ProcessEnv = { ...process.env }; + if (options.prefix) { + env.npm_config_prefix = options.prefix; + } + const execOptions: NpmExecOptions = { + cwd: options.cwd, + timeout: options.timeoutMs, + env, + maxBuffer: 10 * 1024 * 1024, // 10 MB for npm output + }; + if (platform === "win32") { + execOptions.shell = true; + } + return execOptions; +} + +/** + * Runs npm with the given args array. Never uses shell interpolation: argv holds + * only static flags, and any install prefix is passed via `options.prefix` + * (exported as `npm_config_prefix`), not as an argv path. See + * {@link buildNpmExecOptions} for the Windows/Node-24 shell handling. + */ export function runNpm( args: string[], - options: { cwd?: string; timeoutMs?: number } = {} + options: { cwd?: string; timeoutMs?: number; prefix?: string } = {} ): Promise { const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; // On Windows, npm is npm.cmd; on Unix it's npm. @@ -86,12 +144,11 @@ export function runNpm( execFile( npmBin, args, - { + buildNpmExecOptions(process.platform, { cwd: options.cwd, - timeout: timeoutMs, - env: process.env, - maxBuffer: 10 * 1024 * 1024, // 10 MB for npm output - }, + timeoutMs, + prefix: options.prefix, + }), (err, stdout, stderr) => { if (err) { const classified = classifyError( diff --git a/src/lib/skills/schemas.ts b/src/lib/skills/schemas.ts index 6c4621beb9..38c8437796 100644 --- a/src/lib/skills/schemas.ts +++ b/src/lib/skills/schemas.ts @@ -41,7 +41,3 @@ export const SkillConfigSchema = z.object({ timeout: z.number().int().positive().default(30000), maxRetries: z.number().int().min(0).default(3), }); - -export type SkillCreateInput = z.infer; -export type SkillUpdateInput = z.infer; -export type SkillConfig = z.infer; diff --git a/src/lib/skills/skillssh.ts b/src/lib/skills/skillssh.ts index 9d80e96883..1752399d5b 100644 --- a/src/lib/skills/skillssh.ts +++ b/src/lib/skills/skillssh.ts @@ -18,7 +18,6 @@ export const SkillsShSearchResponseSchema = z.object({ duration_ms: z.number().optional(), }); -export type SkillsShSkill = z.infer; export type SkillsShSearchResponse = z.infer; const SKILLSSH_BASE_URL = "https://skills.sh/api"; diff --git a/src/lib/vscode/familyFirstModelIds.ts b/src/lib/vscode/familyFirstModelIds.ts new file mode 100644 index 0000000000..051707ab49 --- /dev/null +++ b/src/lib/vscode/familyFirstModelIds.ts @@ -0,0 +1,92 @@ +const FAMILY_FIRST_MODEL_PATTERN = + /^((?:gpt-[a-z0-9._-]+|claude[a-z0-9._-]*))(?:__provider_([a-z0-9-]+))(?:__tier_(priority|flex))?$/i; +const TIER_SUFFIX_PATTERN = /(__tier_(?:priority|flex))$/i; + +function normalizeFamily(value: string | null | undefined) { + return typeof value === "string" ? value.trim() : ""; +} + +function splitActualModelId(modelId: string) { + const trimmedModelId = modelId.trim(); + const slashIndex = trimmedModelId.indexOf("/"); + if (slashIndex <= 0 || slashIndex === trimmedModelId.length - 1) { + return null; + } + + return { + providerPrefix: trimmedModelId.slice(0, slashIndex), + providerModelId: trimmedModelId.slice(slashIndex + 1), + }; +} + +function extractTierSuffix(modelId: string) { + const match = modelId.match(TIER_SUFFIX_PATTERN); + return match?.[1] || ""; +} + +function stripTierSuffix(modelId: string) { + return modelId.replace(TIER_SUFFIX_PATTERN, ""); +} + +function isFamilyFirstEligibleFamily(family: string) { + const normalized = family.toLowerCase(); + return normalized.startsWith("gpt-") || normalized.startsWith("claude"); +} + +export function getFamilyFirstPublishedModelId( + actualModelId: string, + family: string | null | undefined +) { + const normalizedFamily = normalizeFamily(family); + if (!normalizedFamily || !isFamilyFirstEligibleFamily(normalizedFamily)) { + return actualModelId; + } + + const parts = splitActualModelId(actualModelId); + if (!parts) { + return actualModelId; + } + + const tierSuffix = extractTierSuffix(parts.providerModelId); + const providerModelBase = stripTierSuffix(parts.providerModelId); + if (providerModelBase !== normalizedFamily) { + return actualModelId; + } + + return `${normalizedFamily}__provider_${parts.providerPrefix}${tierSuffix}`; +} + +export function resolveFamilyFirstPublishedModelId(modelId: string | null | undefined) { + const trimmedModelId = typeof modelId === "string" ? modelId.trim() : ""; + if (!trimmedModelId) { + return trimmedModelId; + } + + const match = trimmedModelId.match(FAMILY_FIRST_MODEL_PATTERN); + if (!match) { + return trimmedModelId; + } + + const [, family, providerPrefix, serviceTier] = match; + const tierSuffix = serviceTier ? `__tier_${serviceTier.toLowerCase()}` : ""; + return `${providerPrefix}/${family}${tierSuffix}`; +} + +export function getFamilyFirstModelCandidates( + actualModelId: string, + family: string | null | undefined +) { + const normalizedFamily = normalizeFamily(family); + const candidates = new Set([actualModelId]); + const publishedModelId = getFamilyFirstPublishedModelId(actualModelId, normalizedFamily); + if (publishedModelId !== actualModelId) { + candidates.add(publishedModelId); + } + + if (normalizedFamily && isFamilyFirstEligibleFamily(normalizedFamily)) { + const tierSuffix = extractTierSuffix(actualModelId); + candidates.add(`${normalizedFamily}${tierSuffix}`); + } + + return [...candidates]; +} diff --git a/src/lib/vscode/modelPresentation.ts b/src/lib/vscode/modelPresentation.ts new file mode 100644 index 0000000000..980f8722c9 --- /dev/null +++ b/src/lib/vscode/modelPresentation.ts @@ -0,0 +1,132 @@ +import { parseModel } from "@omniroute/open-sse/services/model"; +import { + getCanonicalModelMetadata, + type CanonicalModelMetadata, +} from "@/lib/modelMetadataRegistry"; +import { resolveFamilyFirstPublishedModelId } from "@/lib/vscode/familyFirstModelIds"; +import { getReasoningVariantBaseModelId } from "@/lib/vscode/reasoningMetadata"; +import { + getVscodeServiceTierVariantSuffix, + parseVscodeServiceTierVariantModelId, + supportsVscodeServiceTierVariants, +} from "@/lib/vscode/serviceTierVariants"; + +type VscodeCatalogModel = { + id?: string; + name?: string; + root?: string; + owned_by?: string; +}; + +const PROVIDER_NAME_OVERRIDES: Record = { + codex: "Codex", + cx: "Codex", + github: "GitHub", + gh: "GitHub", + gemini: "Gemini", +}; + +function tokenize(value: string) { + return value + .toLowerCase() + .split(/[^a-z0-9]+/i) + .map((part) => part.trim()) + .filter((part) => part.length >= 4); +} + +function getProviderPrefix(metadata: CanonicalModelMetadata | null) { + const providerKey = metadata?.providerAlias || metadata?.provider || ""; + if (providerKey && PROVIDER_NAME_OVERRIDES[providerKey]) { + return PROVIDER_NAME_OVERRIDES[providerKey]; + } + + const providerLabel = metadata?.providerLabel?.trim() || null; + if (!providerLabel) { + return null; + } + if (/codex/i.test(providerLabel)) { + return "Codex"; + } + if (/github/i.test(providerLabel)) { + return "GitHub"; + } + if (/gemini/i.test(providerLabel)) { + return "Gemini"; + } + + return providerLabel; +} + +function normalizeDisplayNameBranding(displayName: string) { + return displayName + .replace(/^OpenAI\s+Codex\b/i, "Codex") + .replace(/^GitHub\s+Copilot\b/i, "GitHub") + .trim(); +} + +function stripLeadingProviderPrefix(displayName: string, providerPrefix: string | null) { + if (!providerPrefix) { + return displayName; + } + + const escapedProviderPrefix = providerPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return displayName.replace(new RegExp(`^${escapedProviderPrefix}\\s+`, "i"), "").trim(); +} + +function prefixDisplayName(displayName: string, providerPrefix: string | null) { + const normalizedProviderPrefix = providerPrefix?.trim() || null; + const normalizedDisplayName = normalizeDisplayNameBranding(displayName); + + if (!normalizedProviderPrefix) return normalizedDisplayName; + + const providerTokens = tokenize(normalizedProviderPrefix); + if (providerTokens.length === 0) return normalizedDisplayName; + + const displayNameLower = normalizedDisplayName.toLowerCase(); + if (providerTokens.some((token) => displayNameLower.includes(token))) { + return normalizedDisplayName; + } + + return `${normalizedProviderPrefix} ${stripLeadingProviderPrefix(normalizedDisplayName, normalizedProviderPrefix)}`.trim(); +} + +export function resolveVscodeModelMetadata(model: VscodeCatalogModel) { + const rawModelId = model.id || model.root || model.name || ""; + const normalizedModelId = resolveFamilyFirstPublishedModelId(rawModelId); + const parsedTierModel = parseVscodeServiceTierVariantModelId(normalizedModelId); + const canonicalBaseModelId = getReasoningVariantBaseModelId(parsedTierModel.baseModelId); + const parsed = parseModel(canonicalBaseModelId, ""); + const provider = parsed.provider || model.owned_by || undefined; + const providerModel = + parsed.model || + (canonicalBaseModelId.includes("/") + ? canonicalBaseModelId.split("/").slice(1).join("/") + : canonicalBaseModelId) || + model.root || + model.id || + model.name || + undefined; + + return providerModel && provider + ? getCanonicalModelMetadata({ provider, model: providerModel }) + : providerModel + ? getCanonicalModelMetadata({ model: providerModel }) + : null; +} + +export function getVscodeModelDisplayName(model: VscodeCatalogModel) { + const rawModelId = model.id || model.root || model.name || ""; + const { serviceTier } = parseVscodeServiceTierVariantModelId(rawModelId); + const metadata = resolveVscodeModelMetadata(model); + const displayName = metadata?.displayName || model.name || model.id || model.root || "unknown"; + const prefixedDisplayName = prefixDisplayName(displayName, getProviderPrefix(metadata)); + const shouldShowTierSuffix = Boolean(serviceTier) || supportsVscodeServiceTierVariants(model); + return shouldShowTierSuffix + ? `${prefixedDisplayName} (${getVscodeServiceTierVariantSuffix(serviceTier)})` + : prefixedDisplayName; +} + +export function getVscodeModelGroupingKey(model: VscodeCatalogModel) { + const metadata = resolveVscodeModelMetadata(model); + return metadata?.qualifiedId || metadata?.model || model.id || model.name || model.root || ""; +} diff --git a/src/lib/vscode/reasoningMetadata.ts b/src/lib/vscode/reasoningMetadata.ts new file mode 100644 index 0000000000..60c1d30c1f --- /dev/null +++ b/src/lib/vscode/reasoningMetadata.ts @@ -0,0 +1,183 @@ +import { supportsXHighEffort } from "@omniroute/open-sse/config/providerModels"; +import { parseModel } from "@omniroute/open-sse/services/model"; +import { stripVscodeServiceTierVariantModelId } from "@/lib/vscode/serviceTierVariants"; + +export type VscodeCatalogModel = { + id?: string; + name?: string; + root?: string; + owned_by?: string; + capabilities?: Record; + supportsReasoningEffort?: string[]; + supportedReasoningEfforts?: string[]; + supports_reasoning_effort?: string[]; + defaultReasoningEffort?: string; + default_reasoning_effort?: string; +}; + +const EFFORT_SUFFIX_PATTERN = /-(xhigh|high|medium|low|none)$/i; +const DEFAULT_REASONING_EFFORT = "none"; +const KNOWN_REASONING_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh"]); + +export type VscodeModelConfigSchema = { + type: "object"; + properties: { + reasoningEffort: { + type: "string"; + title: string; + description: string; + default: string; + enum: string[]; + enumLabels: string[]; + enumDescriptions: string[]; + }; + }; +}; + +export function getCatalogModelName(model: VscodeCatalogModel) { + return stripVscodeServiceTierVariantModelId(model.id || model.name || model.root || ""); +} + +function normalizeReasoningEffortValue(value: string) { + const normalized = value + .trim() + .toLowerCase() + .replace(/[_\s-]+/g, ""); + if (normalized === "xhigh") return "xhigh"; + if (KNOWN_REASONING_EFFORTS.has(normalized)) return normalized; + return undefined; +} + +function getNativeReasoningEffortValues(model: VscodeCatalogModel) { + const candidates = [ + model.supportsReasoningEffort, + model.supportedReasoningEfforts, + model.supports_reasoning_effort, + ]; + + for (const candidate of candidates) { + if (!Array.isArray(candidate) || candidate.length === 0) { + continue; + } + + const normalized = Array.from( + new Set( + candidate + .map((value) => + typeof value === "string" ? normalizeReasoningEffortValue(value) : undefined + ) + .filter(Boolean) + ) + ) as string[]; + + if (normalized.length > 0) { + return normalized; + } + } + + return undefined; +} + +export function isReasoningCapableModel(model: VscodeCatalogModel) { + return ( + model.capabilities?.reasoning === true || + model.capabilities?.thinking === true || + (getNativeReasoningEffortValues(model)?.length || 0) > 0 + ); +} + +export function getReasoningEffortValues(model: VscodeCatalogModel) { + const nativeReasoningEffortValues = getNativeReasoningEffortValues(model); + if (nativeReasoningEffortValues && nativeReasoningEffortValues.length > 0) { + return nativeReasoningEffortValues; + } + + if (!isReasoningCapableModel(model)) return undefined; + + const modelId = getCatalogModelName(model); + const parsed = parseModel(modelId, ""); + const providerId = parsed.provider || model.owned_by || ""; + const providerModelId = parsed.model || model.root || modelId.split("/").pop() || modelId; + const values = ["none", "low", "medium", "high"]; + + if (providerId && providerModelId && supportsXHighEffort(providerId, providerModelId)) { + values.push("xhigh"); + } + + return values; +} + +export function formatReasoningEffortLabel(level: string) { + if (level === "xhigh") return "XHigh"; + return level.charAt(0).toUpperCase() + level.slice(1); +} + +function describeReasoningEffort(level: string) { + switch (level) { + case "none": + return "Disables extra reasoning effort."; + case "low": + return "Uses a light amount of reasoning."; + case "medium": + return "Uses a balanced amount of reasoning."; + case "high": + return "Uses an extended amount of reasoning."; + case "xhigh": + return "Uses the maximum available reasoning effort."; + default: + return `Uses ${formatReasoningEffortLabel(level)} reasoning effort.`; + } +} + +export function buildSupportedReasoningEfforts(supportedValues: string[]): string[] { + return [...supportedValues]; +} + +export function inferSelectedReasoningEffort( + model: VscodeCatalogModel, + supportedValues?: string[] +) { + const modelId = getCatalogModelName(model); + const match = modelId.match(EFFORT_SUFFIX_PATTERN); + if (!match) return undefined; + + const selected = match[1]?.toLowerCase(); + if (!selected) return undefined; + if ( + Array.isArray(supportedValues) && + supportedValues.length > 0 && + !supportedValues.includes(selected) + ) { + return undefined; + } + + return selected; +} + +export function getReasoningVariantBaseModelId(modelId: string) { + return modelId.replace(EFFORT_SUFFIX_PATTERN, ""); +} + +export function getDefaultReasoningEffort(model: VscodeCatalogModel, supportedValues?: string[]) { + return inferSelectedReasoningEffort(model, supportedValues) || DEFAULT_REASONING_EFFORT; +} + +export function buildReasoningConfigSchema( + supportedValues: string[], + defaultReasoningEffort: string +): VscodeModelConfigSchema { + return { + type: "object", + properties: { + reasoningEffort: { + type: "string", + title: "Reasoning effort", + description: "Controls how much reasoning effort the model uses.", + default: defaultReasoningEffort, + enum: supportedValues, + enumLabels: supportedValues.map(formatReasoningEffortLabel), + enumDescriptions: supportedValues.map(describeReasoningEffort), + }, + }, + }; +} diff --git a/src/lib/vscode/serviceTierVariants.ts b/src/lib/vscode/serviceTierVariants.ts new file mode 100644 index 0000000000..d5032127bc --- /dev/null +++ b/src/lib/vscode/serviceTierVariants.ts @@ -0,0 +1,202 @@ +import { CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS } from "@/lib/providers/codexFastTier"; +import { resolveFamilyFirstPublishedModelId } from "@/lib/vscode/familyFirstModelIds"; +import { normalizeServiceTierId, type ServiceTierId } from "@/shared/utils/serviceTierLabels"; + +const SERVICE_TIER_VARIANT_PATTERN = /__tier_(priority|flex)$/i; +const SUPPORTED_VSCODE_SERVICE_TIERS: readonly ServiceTierId[] = ["priority", "flex"]; + +export type VscodeServiceTierModelLike = { + id?: string; + name?: string; + root?: string; + owned_by?: string; +}; + +export function parseVscodeServiceTierVariantModelId(modelId: string | null | undefined): { + baseModelId: string; + serviceTier?: ServiceTierId; +} { + const rawModelId = typeof modelId === "string" ? modelId.trim() : ""; + if (!rawModelId) { + return { baseModelId: "" }; + } + + const match = rawModelId.match(SERVICE_TIER_VARIANT_PATTERN); + if (!match) { + return { baseModelId: rawModelId }; + } + + const baseModelId = rawModelId.replace(SERVICE_TIER_VARIANT_PATTERN, ""); + const serviceTier = normalizeServiceTierId(match[1]); + return serviceTier === "standard" ? { baseModelId } : { baseModelId, serviceTier }; +} + +export function stripVscodeServiceTierVariantModelId(modelId: string | null | undefined): string { + return parseVscodeServiceTierVariantModelId(modelId).baseModelId; +} + +export function isVscodeServiceTierVariantModelId(modelId: string | null | undefined): boolean { + return Boolean(parseVscodeServiceTierVariantModelId(modelId).serviceTier); +} + +export function getVscodeServiceTierVariantModelId( + baseModelId: string, + serviceTier: ServiceTierId +): string { + if (serviceTier === "standard") { + return baseModelId; + } + return `${baseModelId}__tier_${serviceTier}`; +} + +function getRawModelId(model: VscodeServiceTierModelLike): string { + return (model.id || model.name || model.root || "").trim(); +} + +function getModelProvider(model: VscodeServiceTierModelLike, baseModelId: string): string { + const owner = typeof model.owned_by === "string" ? model.owned_by.trim().toLowerCase() : ""; + if (owner) { + return owner; + } + const prefix = baseModelId.split("/")[0]?.trim().toLowerCase() || ""; + return prefix; +} + +function supportsCodexServiceTierModel(baseModelId: string): boolean { + const normalizedModel = (baseModelId.split("/").pop() || baseModelId).trim().toLowerCase(); + if (!normalizedModel) { + return false; + } + + return CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS.some((candidate) => { + const normalizedCandidate = candidate.trim().toLowerCase(); + return ( + normalizedModel === normalizedCandidate || normalizedModel.startsWith(normalizedCandidate) + ); + }); +} + +export function supportsVscodeServiceTierVariants(model: VscodeServiceTierModelLike): boolean { + const rawModelId = getRawModelId(model); + if (!rawModelId) { + return false; + } + + const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId); + const provider = getModelProvider(model, baseModelId); + if (provider !== "codex" && provider !== "cx") { + return false; + } + + return supportsCodexServiceTierModel(baseModelId); +} + +function cloneModelIdentifiers(model: T, modelId: string): T { + return { + ...model, + ...(model.id ? { id: modelId } : {}), + ...(model.name ? { name: modelId } : {}), + ...(model.root ? { root: modelId } : {}), + }; +} + +export function expandVscodeServiceTierModels( + models: T[] +): T[] { + const expanded: T[] = []; + + for (const model of models) { + const rawModelId = getRawModelId(model); + if (!rawModelId) { + expanded.push(model); + continue; + } + + const baseModelId = stripVscodeServiceTierVariantModelId(rawModelId); + const baseModel = + rawModelId === baseModelId ? model : cloneModelIdentifiers(model, baseModelId); + expanded.push(baseModel as T); + + if (!supportsVscodeServiceTierVariants(model)) { + continue; + } + + for (const serviceTier of SUPPORTED_VSCODE_SERVICE_TIERS) { + expanded.push( + cloneModelIdentifiers( + baseModel as T, + getVscodeServiceTierVariantModelId(baseModelId, serviceTier) + ) + ); + } + } + + return expanded; +} + +export function getVscodeServiceTierVariantSuffix(serviceTier: ServiceTierId | undefined): string { + if (serviceTier === "priority") { + return "Fast"; + } + if (serviceTier === "flex") { + return "Flex"; + } + return "Default"; +} + +export function resolveVscodeServiceTierRequest( + body: Record +): Record { + const rawModelId = typeof body.model === "string" ? body.model.trim() : ""; + if (!rawModelId) { + return body; + } + + const resolvedModelId = resolveFamilyFirstPublishedModelId(rawModelId); + + const { baseModelId, serviceTier } = parseVscodeServiceTierVariantModelId(resolvedModelId); + if (!serviceTier) { + if (resolvedModelId === rawModelId) { + return body; + } + + return { + ...body, + model: resolvedModelId, + }; + } + + return { + ...body, + model: baseModelId, + ...(body.service_tier === undefined ? { service_tier: serviceTier } : {}), + }; +} + +export async function rewriteVscodeServiceTierRequest(request: Request): Promise { + if (request.method !== "POST") { + return request; + } + + const body = await request + .clone() + .json() + .catch(() => null); + if (!body || typeof body !== "object" || Array.isArray(body)) { + return request; + } + + const rewrittenBody = resolveVscodeServiceTierRequest(body as Record); + if (rewrittenBody === body) { + return request; + } + + const headers = new Headers(request.headers); + headers.delete("content-length"); + + return new Request(request.url, { + method: request.method, + headers, + body: JSON.stringify(rewrittenBody), + }); +} diff --git a/src/lib/vscode/tokenizedRequest.ts b/src/lib/vscode/tokenizedRequest.ts new file mode 100644 index 0000000000..24bb41d252 --- /dev/null +++ b/src/lib/vscode/tokenizedRequest.ts @@ -0,0 +1,56 @@ +import { sanitizeVscodeRequest } from "@/app/api/v1/vscode/contextSanitizer"; + +function inferTokenFromVscodePath(request: Request) { + try { + const url = new URL(request.url, "http://localhost"); + const segments = url.pathname + .split("/") + .map((segment) => segment.trim()) + .filter(Boolean); + + const vscodeIndex = segments.indexOf("vscode"); + if (vscodeIndex === -1) return null; + + const candidate = segments[vscodeIndex + 1]; + if (!candidate || candidate === "raw" || candidate === "combos") { + const nestedCandidate = segments[vscodeIndex + 2]; + return nestedCandidate ? decodeURIComponent(nestedCandidate) : null; + } + + return decodeURIComponent(candidate); + } catch { + return null; + } +} + +export function withPathTokenApiKey(request: Request, token?: string) { + const resolvedToken = token || inferTokenFromVscodePath(request); + if (!resolvedToken) return request; + + const headers = new Headers(request.headers); + + if (!headers.has("x-api-key")) { + headers.set("x-api-key", resolvedToken); + } + + if (!headers.has("authorization")) { + headers.set("authorization", `Bearer ${resolvedToken}`); + } + + const method = request.method; + const init: RequestInit & { duplex?: "half" } = { + method, + headers, + }; + + if (method !== "GET" && method !== "HEAD") { + init.body = request.body; + init.duplex = "half"; + } + + return new Request(request.url, init); +} + +export async function withSanitizedPathTokenApiKey(request: Request, token?: string) { + return sanitizeVscodeRequest(withPathTokenApiKey(request, token)); +} diff --git a/src/server/authz/csrf.ts b/src/server/authz/csrf.ts new file mode 100644 index 0000000000..d735c1e17e --- /dev/null +++ b/src/server/authz/csrf.ts @@ -0,0 +1,101 @@ +import { createHash, createHmac, timingSafeEqual } from "node:crypto"; + +import { DASHBOARD_CSRF_HEADER } from "@/shared/constants/dashboardCsrf"; + +const TOKEN_VERSION = "v1"; +const TOKEN_TTL_SECONDS = 10 * 60; +const TOKEN_CONTEXT = "omniroute-dashboard-csrf-v1"; +const TEST_MUTATION_PATHS = new Set([ + "/api/combos/test", + "/api/models/test", + "/api/models/test-all", +]); + +export interface DashboardCsrfToken { + token: string; + expiresAt: string; +} + +function getJwtSecret(): Buffer | null { + const secret = process.env.JWT_SECRET?.trim(); + return secret ? Buffer.from(secret, "utf8") : null; +} + +function getCookieValue(request: Request, name: string): string | null { + const cookieHeader = request.headers.get("cookie") || request.headers.get("Cookie"); + if (!cookieHeader) return null; + + for (const segment of cookieHeader.split(";")) { + const [rawKey, ...rawValue] = segment.split("="); + if (!rawKey || rawValue.length === 0) continue; + if (rawKey.trim() === name) return rawValue.join("=").trim() || null; + } + + return null; +} + +function sessionHash(authToken: string): string { + return createHash("sha256").update(authToken).digest("base64url"); +} + +function csrfMac(secret: Buffer, expiresAtSeconds: number, authToken: string): Buffer { + return createHmac("sha256", secret) + .update(TOKEN_CONTEXT) + .update("\n") + .update(String(expiresAtSeconds)) + .update("\n") + .update(sessionHash(authToken)) + .digest(); +} + +export function issueDashboardCsrfToken( + request: Request, + nowMs: number = Date.now() +): DashboardCsrfToken | null { + const secret = getJwtSecret(); + const authToken = getCookieValue(request, "auth_token"); + if (!secret || !authToken) return null; + + const expiresAtSeconds = Math.floor(nowMs / 1000) + TOKEN_TTL_SECONDS; + const mac = csrfMac(secret, expiresAtSeconds, authToken).toString("base64url"); + + return { + token: `${TOKEN_VERSION}.${expiresAtSeconds}.${mac}`, + expiresAt: new Date(expiresAtSeconds * 1000).toISOString(), + }; +} + +function requestPathname(request: Request): string | null { + try { + return new URL(request.url).pathname; + } catch { + return null; + } +} + +export function validateDashboardCsrfToken(request: Request, nowMs: number = Date.now()): boolean { + const pathname = requestPathname(request); + if (!pathname || !TEST_MUTATION_PATHS.has(pathname)) return false; + + const secret = getJwtSecret(); + const authToken = getCookieValue(request, "auth_token"); + const rawToken = request.headers.get(DASHBOARD_CSRF_HEADER); + if (!secret || !authToken || !rawToken) return false; + + const [version, rawExpiresAt, rawMac, ...extra] = rawToken.split("."); + if (extra.length > 0 || version !== TOKEN_VERSION || !rawExpiresAt || !rawMac) return false; + + const expiresAtSeconds = Number(rawExpiresAt); + const nowSeconds = Math.floor(nowMs / 1000); + if (!Number.isSafeInteger(expiresAtSeconds) || expiresAtSeconds < nowSeconds) return false; + + let providedMac: Buffer; + try { + providedMac = Buffer.from(rawMac, "base64url"); + } catch { + return false; + } + + const expectedMac = csrfMac(secret, expiresAtSeconds, authToken); + return providedMac.length === expectedMac.length && timingSafeEqual(providedMac, expectedMac); +} diff --git a/src/server/authz/pipeline.ts b/src/server/authz/pipeline.ts index 72257947ec..d05567fd52 100644 --- a/src/server/authz/pipeline.ts +++ b/src/server/authz/pipeline.ts @@ -7,6 +7,7 @@ import { generateRequestId } from "../../shared/utils/requestId"; import { applyCorsHeaders } from "../cors/origins"; import { validateBrowserMutationOrigin } from "../origin/publicOrigin"; import { classifyRoute } from "./classify"; +import { validateDashboardCsrfToken } from "./csrf"; import { classifyStampedPeerLocality } from "./peerStamp"; import { clientApiPolicy } from "./policies/clientApi"; import { managementPolicy } from "./policies/management"; @@ -329,7 +330,7 @@ export async function runAuthzPipeline( isUnsafeMutationMethod(method) ) { const originVerdict = validateBrowserMutationOrigin(request); - if (!originVerdict.ok) { + if (!originVerdict.ok && !validateDashboardCsrfToken(request)) { const rejection = invalidOriginResponse(requestId); rejection.headers.set(AUTHZ_HEADER_ROUTE_CLASS, classification.routeClass); applyCorsHeaders(rejection, request); diff --git a/src/server/authz/routeGuard.ts b/src/server/authz/routeGuard.ts index 10dc695e8d..8874ed33f7 100644 --- a/src/server/authz/routeGuard.ts +++ b/src/server/authz/routeGuard.ts @@ -22,6 +22,7 @@ */ import { getAuthzBypassSnapshot } from "@/lib/config/runtimeSettings"; +import { SPAWN_CAPABLE_PREFIXES } from "@/shared/constants/spawnCapablePrefixes"; const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]); @@ -59,31 +60,13 @@ export const LOCAL_ONLY_API_PATTERNS: ReadonlyArray = [ /^\/api\/providers\/[^/]+\/login\/?$/, ]; -/** - * Compile-time deny-list: route prefixes that can spawn arbitrary local - * subprocesses on behalf of the caller. These MUST NEVER appear in the - * manage-scope bypass list — regardless of DB state — because reaching them - * from non-loopback would re-introduce the GHSA-fhh6-4qxv-rpqj surface that - * the LOCAL_ONLY tier exists to close. - * - * Enforced at two layers: - * 1. zod schema (`settingsSchemas.ts`): rejects `PATCH /api/settings` with - * error code `BYPASS_PREFIX_NOT_ALLOWED` if any entry in - * `localOnlyManageScopeBypassPrefixes` falls inside this set. - * 2. runtime (`isLocalOnlyBypassableByManageScope` below): even if a - * malformed DB row somehow claims a spawn-capable path is bypassable, - * the policy still refuses to honour it. - */ -export const SPAWN_CAPABLE_PREFIXES: ReadonlyArray = [ - "/api/cli-tools/runtime/", - "/api/services/", // T-10: can run npm install + spawn node processes - "/api/tools/agent-bridge/", // start/stop MITM server + DNS edits (Hard Rules #15 + #17) - "/api/tools/traffic-inspector/", // http-proxy listener + system proxy (Hard Rules #15 + #17) - "/api/plugins/", // plugins: load/execute via worker_threads + child_process (Hard Rules #15 + #17) - "/api/local/", // T-12: 1-click local service launchers (Redis today) — must never be whitelistable via manage-scope bypass (Hard Rules #15 + #17) - "/api/headroom/start", // spawns headroom-ai python CLI — must never be bypassable (Hard Rules #15 + #17) - "/api/headroom/stop", // kills tracked PID — must never be bypassable (Hard Rules #15 + #17) -]; +// `SPAWN_CAPABLE_PREFIXES` (the spawn-capable deny-list) now lives in the +// server-free leaf module `@/shared/constants/spawnCapablePrefixes` so that +// client-reachable validation schemas can import it without pulling this module's +// server runtime (runtimeSettings → localDb → ioredis) into the browser bundle. +// Imported above for the runtime check in `isLocalOnlyBypassableByManageScope`; +// re-exported here so existing `@/server/authz/routeGuard` importers keep working. +export { SPAWN_CAPABLE_PREFIXES }; /** * Compile-time default of the manage-scope bypass list. Kept as an exported diff --git a/src/shared/components/analytics/rechartsDonuts.tsx b/src/shared/components/analytics/rechartsDonuts.tsx index d0f5f60245..70b7f27a28 100644 --- a/src/shared/components/analytics/rechartsDonuts.tsx +++ b/src/shared/components/analytics/rechartsDonuts.tsx @@ -12,6 +12,78 @@ import { import { PROVIDER_COLORS } from "./chartColors"; import { ChartLoadingCard, DarkTooltip, useRecharts } from "./rechartsCore"; +function CompactDonutCard({ + pieData, + title, + formatter, + valueClassName = "text-text-muted", + labelClassName = "", + getLegendKey, + getLegendTitle, +}) { + const recharts = useRecharts(); + + if (!recharts) { + return ; + } + + const { Cell, PieChart, Pie, Tooltip, ResponsiveContainer } = recharts; + + return ( + +

+ {title} +

+
+ + + + {pieData.map((entry, i) => ( + + ))} + + } /> + + +
+ {pieData.map((seg, i) => ( +
+
+ + + {seg.name} + +
+ + {formatter(seg.value)} + +
+ ))} +
+
+
+ ); +} + // ── AccountDonut (Recharts) ──────────────────────────────────────────────── export function AccountDonut({ byAccount }) { @@ -38,63 +110,7 @@ export function AccountDonut({ byAccount }) { ); } - return ; -} - -function AccountDonutBody({ pieData }) { - const recharts = useRecharts(); - - if (!recharts) { - return ; - } - - const { Cell, PieChart, Pie, Tooltip, ResponsiveContainer } = recharts; - - return ( - -

- By Account -

-
- - - - {pieData.map((entry, i) => ( - - ))} - - } /> - - -
- {pieData.map((seg, i) => ( -
-
- - {seg.name} -
- - {fmt(seg.value)} - -
- ))} -
-
-
- ); + return ; } // ── ApiKeyDonut (Recharts) ───────────────────────────────────────────────── @@ -124,67 +140,14 @@ export function ApiKeyDonut({ byApiKey }) { ); } - return ; -} - -function ApiKeyDonutBody({ pieData }) { - const recharts = useRecharts(); - - if (!recharts) { - return ; - } - - const { Cell, PieChart, Pie, Tooltip, ResponsiveContainer } = recharts; - return ( - -

- By API Key -

-
- - - - {pieData.map((entry, i) => ( - - ))} - - } /> - - -
- {pieData.map((seg, i) => ( -
-
- - - {seg.name} - -
- - {fmt(seg.value)} - -
- ))} -
-
-
+ `${seg.fullName}-${i}`} + getLegendTitle={(seg) => seg.fullName} + /> ); } @@ -218,63 +181,14 @@ export function ProviderCostDonut({ byProvider }) { ); } - return ; -} - -function ProviderCostDonutBody({ pieData }) { - const t = useTranslations("analytics"); - const recharts = useRecharts(); - - if (!recharts) { - return ; - } - - const { Cell, PieChart, Pie, Tooltip, ResponsiveContainer } = recharts; - return ( - -

- {t("chartCostByProvider")} -

-
- - - - {pieData.map((entry, i) => ( - - ))} - - } /> - - -
- {pieData.map((seg, i) => ( -
-
- - {seg.name} -
- - {fmtCost(seg.value)} - -
- ))} -
-
-
+ ); } diff --git a/src/shared/components/lobeProviderIcons.ts b/src/shared/components/lobeProviderIcons.ts index e37bf7f5e3..d709e2a5c4 100644 --- a/src/shared/components/lobeProviderIcons.ts +++ b/src/shared/components/lobeProviderIcons.ts @@ -118,7 +118,6 @@ import OpenCodeMonoIcon from "@lobehub/icons/es/OpenCode/components/Mono"; import OpenRouterMonoIcon from "@lobehub/icons/es/OpenRouter/components/Mono"; import PerplexityColorIcon from "@lobehub/icons/es/Perplexity/components/Color"; import PerplexityMonoIcon from "@lobehub/icons/es/Perplexity/components/Mono"; -import PhindMonoIcon from "@lobehub/icons/es/Phind/components/Mono"; import PoeColorIcon from "@lobehub/icons/es/Poe/components/Color"; import PoeMonoIcon from "@lobehub/icons/es/Poe/components/Mono"; import PollinationsMonoIcon from "@lobehub/icons/es/Pollinations/components/Mono"; @@ -268,7 +267,6 @@ const LOBE_ICON_COMPONENTS = { OpenCode: { mono: OpenCodeMonoIcon }, OpenRouter: { mono: OpenRouterMonoIcon }, Perplexity: { mono: PerplexityMonoIcon, color: PerplexityColorIcon }, - Phind: { mono: PhindMonoIcon }, Poe: { mono: PoeMonoIcon, color: PoeColorIcon }, Pollinations: { mono: PollinationsMonoIcon }, Qoder: { mono: QoderMonoIcon, color: QoderColorIcon }, @@ -426,7 +424,6 @@ const LOBE_PROVIDER_ALIASES = { "perplexity-search": "Perplexity", "perplexity-web": "Perplexity", poe: "Poe", - phind: "Phind", pollinations: "Pollinations", qoder: "Qoder", qwen: "Qwen", diff --git a/src/shared/constants/dashboardCsrf.ts b/src/shared/constants/dashboardCsrf.ts new file mode 100644 index 0000000000..f340f4292e --- /dev/null +++ b/src/shared/constants/dashboardCsrf.ts @@ -0,0 +1 @@ +export const DASHBOARD_CSRF_HEADER = "x-omniroute-csrf"; diff --git a/src/shared/constants/providers/apikey/frontier-labs.ts b/src/shared/constants/providers/apikey/frontier-labs.ts index dc99ff4be1..ca3783c995 100644 --- a/src/shared/constants/providers/apikey/frontier-labs.ts +++ b/src/shared/constants/providers/apikey/frontier-labs.ts @@ -223,19 +223,6 @@ export const APIKEY_PROVIDERS_FRONTIER = { hasFree: true, freeNote: "Free tier: 50 RPM, 500,000 TPM — no credit card", }, - phind: { - id: "phind", - alias: "phind", - name: "Phind", - icon: "search", - color: "#EC4899", - textIcon: "PH", - website: "https://phind.com", - hasFree: false, - freeNote: "Discontinued 2026 — phind.com shut down (2026-01); no free tier.", - passthroughModels: true, - authHint: "Get API key at phind.com", - }, "arcee-ai": { id: "arcee-ai", alias: "arcee", diff --git a/src/shared/constants/providers/apikey/inference-hosts.ts b/src/shared/constants/providers/apikey/inference-hosts.ts index fa6c7b30db..67af6afe56 100644 --- a/src/shared/constants/providers/apikey/inference-hosts.ts +++ b/src/shared/constants/providers/apikey/inference-hosts.ts @@ -249,19 +249,6 @@ export const APIKEY_PROVIDERS_INFERENCE = { hasFree: true, freeNote: "$1 free credits, refreshes every 4 weeks", }, - huggingchat: { - id: "huggingchat", - alias: "huggingchat", - name: "HuggingChat", - icon: "chat", - color: "#FFD21E", - textIcon: "HC", - website: "https://huggingface.co/chat", - hasFree: true, - freeNote: "Free chat with open models (Llama, Mistral, etc.).", - passthroughModels: true, - authHint: "No API key required for basic access.", - }, monsterapi: { id: "monsterapi", alias: "monster", diff --git a/src/shared/constants/providers/apikey/regional.ts b/src/shared/constants/providers/apikey/regional.ts index 8e6f8f55e5..3c1dc808fb 100644 --- a/src/shared/constants/providers/apikey/regional.ts +++ b/src/shared/constants/providers/apikey/regional.ts @@ -138,7 +138,7 @@ export const APIKEY_PROVIDERS_REGIONAL = { website: "https://longcat.chat/platform/docs", hasFree: true, freeNote: - "Free: 5M tokens/day on LongCat-2.0-Preview (Flash models retired 2026-05-29); up to 120M/day via feedback.", + "Free: one-time 10M-token grant after account signup + KYC verification (LongCat-2.0). One-time only — not a recurring daily/monthly allowance.", }, moonshot: { id: "moonshot", diff --git a/src/shared/constants/providers/web-cookie.ts b/src/shared/constants/providers/web-cookie.ts index 97e0d52527..17ef4a1f6d 100644 --- a/src/shared/constants/providers/web-cookie.ts +++ b/src/shared/constants/providers/web-cookie.ts @@ -76,7 +76,7 @@ export const WEB_COOKIE_PROVIDERS = { website: "https://www.meta.ai", hasFree: true, freeNote: "Free with login — Meta AI platform with Llama models.", - authHint: "Paste your abra_sess value or full cookie header from meta.ai", + authHint: "Paste your ecto_1_sess value or full cookie header from meta.ai", }, "claude-web": { id: "claude-web", @@ -200,24 +200,6 @@ export const WEB_COOKIE_PROVIDERS = { "Paste your hf-chat cookie value from huggingface.co/chat (DevTools → Application → Cookies → hf-chat). Optional — works without auth for basic use.", riskNoticeVariant: "webCookie", }, - phind: { - id: "phind", - alias: "ph", - name: "Phind (Free)", - icon: "auto_awesome", - color: "#000000", - textIcon: "PH", - website: "https://www.phind.com", - hasFree: false, - freeNote: "Discontinued 2026 — phind.com shut down (2026-01); no free tier.", - authHint: - "Paste your session cookie from phind.com (DevTools → Application → Cookies). Optional — works with free tier.", - subscriptionRisk: true, - riskNoticeVariant: "deprecated", - deprecated: true, - deprecationReason: - "Phind shut down its API (2026-01); the /api/chat endpoint no longer serves (sweep 2026-06-19).", - }, "poe-web": { id: "poe-web", alias: "poe", diff --git a/src/shared/constants/spawnCapablePrefixes.ts b/src/shared/constants/spawnCapablePrefixes.ts new file mode 100644 index 0000000000..5e83f3d546 --- /dev/null +++ b/src/shared/constants/spawnCapablePrefixes.ts @@ -0,0 +1,35 @@ +/** + * Compile-time deny-list: route prefixes whose handlers can spawn arbitrary local + * subprocesses (npm install, node, MITM server, python CLIs) on behalf of the + * caller. These MUST NEVER appear in the manage-scope bypass list — regardless of + * DB state — because reaching them from non-loopback would re-introduce the + * GHSA-fhh6-4qxv-rpqj surface that the LOCAL_ONLY tier exists to close. + * + * Enforced at two layers: + * 1. zod schema (`settingsSchemas.ts`): rejects `PATCH /api/settings` with error + * code `BYPASS_PREFIX_NOT_ALLOWED` if any entry in + * `localOnlyManageScopeBypassPrefixes` falls inside this set. + * 2. runtime (`isLocalOnlyBypassableByManageScope` in `routeGuard.ts`): even if a + * malformed DB row claims a spawn-capable path is bypassable, the policy refuses. + * + * 🔒 This constant lives in `@/shared/constants` — a server-free leaf module — and + * NOT in `@/server/authz/routeGuard`, on purpose. `settingsSchemas.ts` is reachable + * from client components (dashboard onboarding wizard → validation barrel), and + * importing it from `routeGuard.ts` dragged routeGuard's server runtime + * (runtimeSettings → localDb → apiKeys → rateLimiter → ioredis) into the browser + * bundle, breaking the Next CLI/client webpack build with + * `Module not found: Can't resolve 'dns'/'net'`. Keeping the value here lets both the + * client-safe schema and the server routeGuard import it with no server coupling. + * Regression guard: `tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts`. + * Hard Rules #15 + #17. + */ +export const SPAWN_CAPABLE_PREFIXES: ReadonlyArray = [ + "/api/cli-tools/runtime/", + "/api/services/", // T-10: can run npm install + spawn node processes + "/api/tools/agent-bridge/", // start/stop MITM server + DNS edits (Hard Rules #15 + #17) + "/api/tools/traffic-inspector/", // http-proxy listener + system proxy (Hard Rules #15 + #17) + "/api/plugins/", // plugins: load/execute via worker_threads + child_process (Hard Rules #15 + #17) + "/api/local/", // T-12: 1-click local service launchers (Redis today) — must never be whitelistable via manage-scope bypass (Hard Rules #15 + #17) + "/api/headroom/start", // spawns headroom-ai python CLI — must never be bypassable (Hard Rules #15 + #17) + "/api/headroom/stop", // kills tracked PID — must never be bypassable (Hard Rules #15 + #17) +]; diff --git a/src/shared/constants/statusColors.ts b/src/shared/constants/statusColors.ts index 816f5a824a..d33c9ab926 100644 --- a/src/shared/constants/statusColors.ts +++ b/src/shared/constants/statusColors.ts @@ -15,5 +15,3 @@ export const STATUS_HEX = { error: "#ef4444", muted: "#6b7280", } as const; - -export type StatusHexKey = keyof typeof STATUS_HEX; diff --git a/src/shared/providers/webSessionCredentials.ts b/src/shared/providers/webSessionCredentials.ts index 84990e6ac2..e8eebb03fe 100644 --- a/src/shared/providers/webSessionCredentials.ts +++ b/src/shared/providers/webSessionCredentials.ts @@ -129,13 +129,6 @@ export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = { acceptsFullCookieHeader: true, storageKeys: ["cookie", "hf-chat"], }, - phind: { - kind: "cookie", - credentialName: "phind_session", - placeholder: "phind_session=... or full Cookie header from phind.com", - acceptsFullCookieHeader: true, - storageKeys: ["cookie", "phind_session"], - }, "poe-web": { kind: "cookie", credentialName: "p-b", diff --git a/src/shared/schemas/memory.ts b/src/shared/schemas/memory.ts index 3bb54758fc..f9f2dcb578 100644 --- a/src/shared/schemas/memory.ts +++ b/src/shared/schemas/memory.ts @@ -68,9 +68,9 @@ export const EmbeddingProviderListingSchema = z.object({ id: z.string(), // `provider/model` name: z.string(), dimensions: z.number().nullable(), - }), + }) ), - }), + }) ), }); @@ -123,7 +123,7 @@ export const RetrievePreviewResultSchema = z.object({ tier: z.enum(["fts5", "vector", "hybrid-rrf", "qdrant"]), vecScore: z.number().nullable(), ftsScore: z.number().nullable(), - }), + }) ), resolution: z.object({ embeddingSource: z.enum(["remote", "static", "transformers"]).nullable(), @@ -138,10 +138,5 @@ export const RetrievePreviewResultSchema = z.object({ }); export type MemorySettingsExtended = z.infer; -export type MemoryUpdatePut = z.infer; -export type RetrievePreview = z.infer; -export type MemoryReindex = z.infer; -export type MemorySummarize = z.infer; -export type EmbeddingProviderListings = z.infer; export type MemoryEngineStatus = z.infer; export type RetrievePreviewResult = z.infer; diff --git a/src/shared/schemas/playground.ts b/src/shared/schemas/playground.ts index af7cdac650..228ef1cebf 100644 --- a/src/shared/schemas/playground.ts +++ b/src/shared/schemas/playground.ts @@ -11,7 +11,6 @@ export const PlaygroundPresetRowSchema = z.object({ params_json: z.string(), // JSON serializado (parsed na rota) created_at: z.string().datetime(), }); -export type PlaygroundPresetRow = z.infer; /** Body de POST /api/playground/presets. */ export const PlaygroundPresetCreateSchema = z.object({ diff --git a/src/shared/schemas/qdrant.ts b/src/shared/schemas/qdrant.ts index 744ec41bfe..09f78c502d 100644 --- a/src/shared/schemas/qdrant.ts +++ b/src/shared/schemas/qdrant.ts @@ -37,8 +37,3 @@ export const QdrantHealthResultSchema = z.object({ latencyMs: z.number(), error: z.string().optional(), }); - -export type QdrantSettings = z.infer; -export type QdrantSettingsUpdate = z.infer; -export type QdrantSearch = z.infer; -export type QdrantHealthResult = z.infer; diff --git a/src/shared/schemas/quota.ts b/src/shared/schemas/quota.ts index 99bc49beb9..63ce545193 100644 --- a/src/shared/schemas/quota.ts +++ b/src/shared/schemas/quota.ts @@ -4,12 +4,10 @@ import { PoolAllocationSchema, QuotaDimensionSchema } from "@/lib/quota/dimensio export const GroupCreateSchema = z.object({ name: z.string().min(1).max(120), }); -export type GroupCreate = z.infer; export const GroupRenameSchema = z.object({ name: z.string().min(1).max(120), }); -export type GroupRename = z.infer; export const PoolCreateSchema = z .object({ @@ -26,7 +24,6 @@ export const PoolCreateSchema = z }, { message: "primary connectionId must be one of connectionIds" } ); -export type PoolCreate = z.infer; export const PoolUpdateSchema = z.object({ name: z.string().min(1).max(120).optional(), @@ -35,18 +32,15 @@ export const PoolUpdateSchema = z.object({ groupId: z.string().optional(), connectionIds: z.array(z.string().min(1)).min(1).optional(), }); -export type PoolUpdate = z.infer; export const PlanUpsertSchema = z.object({ dimensions: z.array(QuotaDimensionSchema).min(1), }); -export type PlanUpsert = z.infer; export const QuotaStoreSettingsSchema = z.object({ driver: z.enum(["sqlite", "redis"]), redisUrl: z.string().url().nullable().optional(), }); -export type QuotaStoreSettings = z.infer; export const QuotaPreviewQuerySchema = z.object({ apiKeyId: z.string().min(1), @@ -55,7 +49,6 @@ export const QuotaPreviewQuerySchema = z.object({ estimatedUsd: z.coerce.number().nonnegative().optional(), estimatedRequests: z.coerce.number().int().nonnegative().optional(), }); -export type QuotaPreviewQuery = z.infer; export const AuditLogQuerySchema = z.object({ action: z.string().optional(), @@ -66,4 +59,3 @@ export const AuditLogQuerySchema = z.object({ limit: z.coerce.number().int().min(1).max(500).default(50), offset: z.coerce.number().int().min(0).max(10_000).default(0), }); -export type AuditLogQuery = z.infer; diff --git a/src/shared/utils/dashboardCsrf.ts b/src/shared/utils/dashboardCsrf.ts new file mode 100644 index 0000000000..6b3747bdfe --- /dev/null +++ b/src/shared/utils/dashboardCsrf.ts @@ -0,0 +1,56 @@ +import { DASHBOARD_CSRF_HEADER } from "@/shared/constants/dashboardCsrf"; + +interface CachedDashboardCsrfToken { + token: string; + expiresAtMs: number; +} + +let cachedToken: CachedDashboardCsrfToken | null = null; + +export function __resetDashboardCsrfTokenForTests(): void { + cachedToken = null; +} + +async function getDashboardCsrfToken(): Promise { + const now = Date.now(); + if (cachedToken && cachedToken.expiresAtMs - now > 30_000) { + return cachedToken.token; + } + + let response: Response; + try { + response = await fetch("/api/auth/csrf", { + cache: "no-store", + credentials: "same-origin", + }); + } catch { + return null; + } + if (!response.ok) return null; + + const body = (await response.json().catch(() => null)) as { + token?: unknown; + expiresAt?: unknown; + } | null; + + if (typeof body?.token !== "string" || typeof body.expiresAt !== "string") { + cachedToken = null; + return null; + } + + const expiresAtMs = Date.parse(body.expiresAt); + if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now) { + cachedToken = null; + return null; + } + + cachedToken = { token: body.token, expiresAtMs }; + return cachedToken.token; +} + +export async function withDashboardCsrfHeader(headers?: HeadersInit): Promise { + const result = new Headers(headers); + const token = await getDashboardCsrfToken(); + if (token) result.set(DASHBOARD_CSRF_HEADER, token); + return result; +} diff --git a/src/shared/utils/logger.ts b/src/shared/utils/logger.ts index c4db37a8e7..ff71dd6be5 100644 --- a/src/shared/utils/logger.ts +++ b/src/shared/utils/logger.ts @@ -171,5 +171,3 @@ export const logger = buildLogger(); export function createLogger(module: string) { return logger.child({ module }); } - -export default logger; diff --git a/src/shared/utils/machineId.ts b/src/shared/utils/machineId.ts index 710d791a1f..db41fcce76 100644 --- a/src/shared/utils/machineId.ts +++ b/src/shared/utils/machineId.ts @@ -91,6 +91,33 @@ function getMachineIdRaw(): string { return "unknown-machine"; } +async function getRandomMachineFallbackId() { + try { + const cryptoFallback = await import("crypto"); + return cryptoFallback.randomUUID(); + } catch { + if (typeof globalThis !== "undefined" && globalThis.crypto && globalThis.crypto.randomUUID) { + return globalThis.crypto.randomUUID(); + } + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + let r = 0; + if ( + typeof globalThis !== "undefined" && + globalThis.crypto && + globalThis.crypto.getRandomValues + ) { + const arr = new Uint8Array(1); + globalThis.crypto.getRandomValues(arr); + r = arr[0] % 16; + } else { + r = (Date.now() % 16) | 0; + } + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + } +} + /** * Get consistent machine ID using native registry/OS query with salt * This ensures the same physical machine gets the same ID across runs @@ -113,30 +140,7 @@ export async function getConsistentMachineId(salt = null) { } catch (error) { console.log("Error getting machine ID:", error); // Fallback to random ID if node-machine-id fails - try { - const cryptoFallback = await import("crypto"); - return cryptoFallback.randomUUID(); - } catch { - if (typeof globalThis !== "undefined" && globalThis.crypto && globalThis.crypto.randomUUID) { - return globalThis.crypto.randomUUID(); - } - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { - let r = 0; - if ( - typeof globalThis !== "undefined" && - globalThis.crypto && - globalThis.crypto.getRandomValues - ) { - const arr = new Uint8Array(1); - globalThis.crypto.getRandomValues(arr); - r = arr[0] % 16; - } else { - r = (Date.now() % 16) | 0; - } - const v = c === "x" ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); - } + return getRandomMachineFallbackId(); } } @@ -150,30 +154,7 @@ export async function getRawMachineId() { } catch (error) { console.log("Error getting raw machine ID:", error); // Fallback to random ID if node-machine-id fails - try { - const cryptoFallback = await import("crypto"); - return cryptoFallback.randomUUID(); - } catch { - if (typeof globalThis !== "undefined" && globalThis.crypto && globalThis.crypto.randomUUID) { - return globalThis.crypto.randomUUID(); - } - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { - let r = 0; - if ( - typeof globalThis !== "undefined" && - globalThis.crypto && - globalThis.crypto.getRandomValues - ) { - const arr = new Uint8Array(1); - globalThis.crypto.getRandomValues(arr); - r = arr[0] % 16; - } else { - r = (Date.now() % 16) | 0; - } - const v = c === "x" ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); - } + return getRandomMachineFallbackId(); } } diff --git a/src/shared/validation/schemas/apiV1.ts b/src/shared/validation/schemas/apiV1.ts index 0f4bf85475..e941a3dc54 100644 --- a/src/shared/validation/schemas/apiV1.ts +++ b/src/shared/validation/schemas/apiV1.ts @@ -231,42 +231,6 @@ export const searchResultSchema = z.object({ provider_raw: z.record(z.string(), z.unknown()).nullable().optional(), }); -export const v1SearchResponseSchema = z.object({ - id: z.string(), - provider: z.string(), - query: z.string(), - results: z.array(searchResultSchema), - cached: z.boolean(), - answer: z - .object({ - source: z.enum(["none", "provider", "internal"]).optional(), - text: z.string().nullable().optional(), - model: z.string().nullable().optional(), - }) - .nullable() - .optional(), - usage: z.object({ - queries_used: z.number().int().min(0), - search_cost_usd: z.number().min(0), - llm_tokens: z.number().int().min(0).optional(), - }), - metrics: z.object({ - response_time_ms: z.number().int().min(0), - upstream_latency_ms: z.number().int().min(0).optional(), - gateway_latency_ms: z.number().int().min(0).optional(), - total_results_available: z.number().int().nullable(), - }), - errors: z - .array( - z.object({ - provider: z.string(), - code: z.string(), - message: z.string(), - }) - ) - .optional(), -}); - export const v1BatchCreateSchema = z.object({ input_file_id: z.string().min(1), endpoint: z.enum(SUPPORTED_BATCH_ENDPOINTS), @@ -292,4 +256,4 @@ export const v1WebFetchSchema = z.object({ depth: z.union([z.literal(0), z.literal(1), z.literal(2)]).default(0), wait_for_selector: z.string().max(256).optional(), include_metadata: z.boolean().default(false), -}); \ No newline at end of file +}); diff --git a/src/shared/validation/schemas/auth.ts b/src/shared/validation/schemas/auth.ts index 41ee0a8a7b..2e7e6bb1ac 100644 --- a/src/shared/validation/schemas/auth.ts +++ b/src/shared/validation/schemas/auth.ts @@ -133,7 +133,10 @@ export const oauthPollSchema = z.object({ /** Import a raw API token (e.g. WINDSURF_API_KEY) without going through the browser OAuth flow. */ export const oauthImportTokenSchema = z.object({ - token: z.union([z.string().trim().min(1, "Token is required"), z.record(z.string(), z.unknown())]), + token: z.union([ + z.string().trim().min(1, "Token is required"), + z.record(z.string(), z.unknown()), + ]), connectionId: z.string().optional(), }); @@ -192,12 +195,6 @@ export const kiroImportSchema = z.object({ profileArn: z.string().optional(), }); -export const kiroSocialExchangeSchema = z.object({ - code: z.string().trim().min(1, "Code is required"), - codeVerifier: z.string().trim().min(1, "Code verifier is required"), - provider: z.enum(["google", "github"]), -}); - export const zedImportSchema = z.object({ confirmedAccounts: z.array(confirmedAccountSchema), }); diff --git a/src/shared/validation/schemas/provider.ts b/src/shared/validation/schemas/provider.ts index 27d0a32648..3f8f06bd48 100644 --- a/src/shared/validation/schemas/provider.ts +++ b/src/shared/validation/schemas/provider.ts @@ -7,329 +7,21 @@ import { SUPPORTED_BATCH_ENDPOINTS } from "@/shared/constants/batchEndpoints"; import { MAX_REQUEST_BODY_LIMIT_MB, MIN_REQUEST_BODY_LIMIT_MB } from "@/shared/constants/bodySize"; import { COMBO_CONFIG_MODES } from "@/shared/constants/comboConfigMode"; import { providerAllowsOptionalApiKey } from "@/shared/constants/providers"; -import { - OPENROUTER_PRESET_MAX_LENGTH, - isOpenRouterPresetValue, -} from "@/shared/constants/openRouterPreset"; import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility"; import { isForbiddenUpstreamHeaderName, isForbiddenCustomHeaderName, } from "@/shared/constants/upstreamHeaders"; import { MAX_TIMER_TIMEOUT_MS } from "@/shared/utils/runtimeTimeouts"; +import { validateProviderSpecificData } from "@/shared/validation/providerSpecificData"; import { - isHttpUrl, - CODEX_REASONING_EFFORT_VALUES, - REQUEST_DEFAULT_SERVICE_TIER_VALUES, upstreamHeadersRecordSchema, modelCompatPerProtocolSchema, customHeadersSchema, } from "./misc.ts"; -export function validateProviderSpecificData( - data: Record | undefined, - ctx: z.RefinementCtx -): void { - if (!data) return; - - const baseUrl = data.baseUrl; - if (baseUrl !== undefined && (typeof baseUrl !== "string" || !isHttpUrl(baseUrl))) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.baseUrl must be a valid http(s) URL", - path: ["baseUrl"], - }); - } - - const customUserAgent = data.customUserAgent; - if ( - customUserAgent !== undefined && - customUserAgent !== null && - (typeof customUserAgent !== "string" || customUserAgent.length > 500) - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.customUserAgent must be a string up to 500 chars", - path: ["customUserAgent"], - }); - } - - const cx = data.cx; - if (cx !== undefined && cx !== null && (typeof cx !== "string" || cx.length > 500)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.cx must be a string up to 500 chars", - path: ["cx"], - }); - } - - const region = data.region; - if ( - region !== undefined && - region !== null && - (typeof region !== "string" || region.length > 64) - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.region must be a string up to 64 chars", - path: ["region"], - }); - } - - const openaiStoreEnabled = data.openaiStoreEnabled; - if (openaiStoreEnabled !== undefined && typeof openaiStoreEnabled !== "boolean") { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.openaiStoreEnabled must be a boolean", - path: ["openaiStoreEnabled"], - }); - } - - const blockExtraUsage = data.blockExtraUsage; - if (blockExtraUsage !== undefined && typeof blockExtraUsage !== "boolean") { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.blockExtraUsage must be a boolean", - path: ["blockExtraUsage"], - }); - } - - const autoFetchModels = data.autoFetchModels; - if (autoFetchModels !== undefined && typeof autoFetchModels !== "boolean") { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.autoFetchModels must be a boolean", - path: ["autoFetchModels"], - }); - } - - const disableStreamOptions = data.disableStreamOptions; - if (disableStreamOptions !== undefined && typeof disableStreamOptions !== "boolean") { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.disableStreamOptions must be a boolean", - path: ["disableStreamOptions"], - }); - } - - const preset = data.preset; - if (preset !== undefined && preset !== null && !isOpenRouterPresetValue(preset)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `providerSpecificData.preset must be a string up to ${OPENROUTER_PRESET_MAX_LENGTH} chars`, - path: ["preset"], - }); - } - - const requestDefaults = data.requestDefaults; - if (requestDefaults !== undefined) { - if (!requestDefaults || typeof requestDefaults !== "object" || Array.isArray(requestDefaults)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.requestDefaults must be an object", - path: ["requestDefaults"], - }); - } else { - const requestDefaultsRecord = requestDefaults as Record; - const reasoningEffort = requestDefaultsRecord.reasoningEffort; - if ( - reasoningEffort !== undefined && - reasoningEffort !== null && - (typeof reasoningEffort !== "string" || - !CODEX_REASONING_EFFORT_VALUES.has(reasoningEffort.trim().toLowerCase())) - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: - "providerSpecificData.requestDefaults.reasoningEffort must be one of none, low, medium, high, xhigh", - path: ["requestDefaults", "reasoningEffort"], - }); - } - - const serviceTier = requestDefaultsRecord.serviceTier; - if ( - serviceTier !== undefined && - serviceTier !== null && - (typeof serviceTier !== "string" || - !REQUEST_DEFAULT_SERVICE_TIER_VALUES.has(serviceTier.trim().toLowerCase())) - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: - "providerSpecificData.requestDefaults.serviceTier must be one of default, priority, fast, flex when provided", - path: ["requestDefaults", "serviceTier"], - }); - } - - const context1m = requestDefaultsRecord.context1m; - if (context1m !== undefined && context1m !== null && typeof context1m !== "boolean") { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.requestDefaults.context1m must be a boolean", - path: ["requestDefaults", "context1m"], - }); - } - - for (const booleanKey of ["redactThinking", "summarizeThinking"] as const) { - const value = requestDefaultsRecord[booleanKey]; - if (value === undefined || value === null || typeof value === "boolean") continue; - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `providerSpecificData.requestDefaults.${booleanKey} must be a boolean`, - path: ["requestDefaults", booleanKey], - }); - } - } - } - - // [Oracle CONDITIONAL] consoleApiKey는 bailian-coding-plan 전용 필드. - // 다른 프로바이더 공통 규약으로 재사용하지 않는다. - const consoleApiKey = data.consoleApiKey; - if (consoleApiKey !== undefined && consoleApiKey !== null && typeof consoleApiKey !== "string") { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.consoleApiKey must be a string", - path: ["consoleApiKey"], - }); - } - if (typeof consoleApiKey === "string" && consoleApiKey.length > 10000) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.consoleApiKey must be at most 10000 characters", - path: ["consoleApiKey"], - }); - } - - for (const key of ["openCodeGoWorkspaceId", "opencodeGoWorkspaceId", "workspaceId"] as const) { - const value = data[key]; - if (value !== undefined && value !== null && typeof value !== "string") { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `providerSpecificData.${key} must be a string`, - path: [key], - }); - } - if (typeof value === "string" && value.length > 1000) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `providerSpecificData.${key} must be at most 1000 characters`, - path: [key], - }); - } - } - - for (const key of [ - "openCodeGoAuthCookie", - "opencodeGoAuthCookie", - "authCookie", - "ollamaUsageCookie", - "ollamaCloudUsageCookie", - "ollamaCloudCookie", - "usageCookie", - ] as const) { - const value = data[key]; - if (value !== undefined && value !== null && typeof value !== "string") { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `providerSpecificData.${key} must be a string`, - path: [key], - }); - } - if (typeof value === "string" && value.length > 10000) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `providerSpecificData.${key} must be at most 10000 characters`, - path: [key], - }); - } - } - - const groupTag = data.tag; - if ( - groupTag !== undefined && - groupTag !== null && - (typeof groupTag !== "string" || groupTag.length > 100) - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.tag must be a string up to 100 chars", - path: ["tag"], - }); - } - - const routingTags = data.tags; - if (routingTags !== undefined && routingTags !== null) { - if (!Array.isArray(routingTags) || routingTags.length > 50) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.tags must be an array with at most 50 items", - path: ["tags"], - }); - } else if ( - routingTags.some( - (tag) => typeof tag !== "string" || tag.trim().length === 0 || tag.trim().length > 64 - ) - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: - "providerSpecificData.tags must contain non-empty strings up to 64 characters each", - path: ["tags"], - }); - } - } - - const excludedModels = data.excludedModels ?? data.excluded_models; - if (excludedModels !== undefined && excludedModels !== null) { - if (typeof excludedModels === "string") { - if (excludedModels.length > 5000) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.excludedModels string must be up to 5000 chars", - path: ["excludedModels"], - }); - } - } else if (!Array.isArray(excludedModels) || excludedModels.length > 100) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "providerSpecificData.excludedModels must be an array with at most 100 items", - path: ["excludedModels"], - }); - } else if ( - excludedModels.some( - (pattern) => - typeof pattern !== "string" || - pattern.trim().length === 0 || - pattern.trim().length > 200 || - pattern.trim() === "**" - ) - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: - "providerSpecificData.excludedModels must contain non-empty patterns up to 200 characters", - path: ["excludedModels"], - }); - } - } - - const clientProfile = data.clientProfile; - if (clientProfile !== undefined && clientProfile !== null) { - const normalized = typeof clientProfile === "string" ? clientProfile.trim().toLowerCase() : ""; - if ( - typeof clientProfile !== "string" || - !["ide", "harness", "cli", "sdk"].includes(normalized) - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: - "providerSpecificData.clientProfile must be ide, harness, cli, or sdk (cli/sdk map to harness)", - path: ["clientProfile"], - }); - } - } -} +export { validateProviderSpecificData }; // ──── Provider Schemas ──── diff --git a/src/shared/validation/settingsSchemas.ts b/src/shared/validation/settingsSchemas.ts index 60fcb6c9d2..3d53665875 100644 --- a/src/shared/validation/settingsSchemas.ts +++ b/src/shared/validation/settingsSchemas.ts @@ -12,10 +12,84 @@ import { HIDEABLE_SIDEBAR_GROUP_IDS } from "@/shared/constants/sidebarGroupVisib import { HIDEABLE_SIDEBAR_ITEM_IDS, SIDEBAR_SECTIONS } from "@/shared/constants/sidebarVisibility"; import { ACCOUNT_FALLBACK_STRATEGY_VALUES } from "@/shared/constants/routingStrategies"; import { RESPONSES_PREVIOUS_RESPONSE_ID_MODES } from "@/shared/constants/responsesPreviousResponseId"; -import { SPAWN_CAPABLE_PREFIXES } from "@/server/authz/routeGuard"; +// Import from the server-free constants leaf, NOT from `@/server/authz/routeGuard`: +// this schema is reachable from client components (dashboard onboarding wizard), and +// routeGuard drags in server runtime (→ ioredis) that breaks the client/CLI build. +import { SPAWN_CAPABLE_PREFIXES } from "@/shared/constants/spawnCapablePrefixes"; const signatureCacheModeValues = ["enabled", "bypass", "bypass-strict"] as const; +const transformDropParagraphIfContainsSchema = z.object({ + kind: z.literal("drop_paragraph_if_contains"), + needles: z.array(z.string().max(500)).max(50), + caseSensitive: z.boolean().optional(), +}); + +const transformDropParagraphIfStartsWithSchema = z.object({ + kind: z.literal("drop_paragraph_if_starts_with"), + prefixes: z.array(z.string().max(500)).max(50), + caseSensitive: z.boolean().optional(), +}); + +const transformReplaceTextSchema = z.object({ + kind: z.literal("replace_text"), + match: z.string().min(1).max(500), + replacement: z.string().max(500), + allOccurrences: z.boolean().optional(), +}); + +const transformReplaceRegexSchema = z.object({ + kind: z.literal("replace_regex"), + pattern: z.string().min(1).max(500), + flags: z.string().max(10).optional(), + replacement: z.string().max(500), +}); + +const transformDropBlockIfContainsSchema = z.object({ + kind: z.literal("drop_block_if_contains"), + needles: z.array(z.string().max(500)).max(50), +}); + +const transformPrependSystemBlockSchema = z.object({ + kind: z.literal("prepend_system_block"), + text: z.string().min(1).max(2000), + idempotencyKey: z.string().max(100).optional(), +}); + +const transformAppendSystemBlockSchema = z.object({ + kind: z.literal("append_system_block"), + text: z.string().min(1).max(2000), + idempotencyKey: z.string().max(100).optional(), +}); + +const transformInjectBillingHeaderSchema = z.object({ + kind: z.literal("inject_billing_header"), + entrypoint: z.string().min(1).max(50), + versionFormat: z.enum(["ex-machina", "omniroute-daystamp"]), + cchAlgo: z.enum(["sha256-first-user", "xxhash64-body", "static-zero"]), + version: z.string().max(50).optional(), +}); + +const commonSystemTransformOperationSchemas = [ + transformDropParagraphIfContainsSchema, + transformDropParagraphIfStartsWithSchema, + transformReplaceTextSchema, + transformReplaceRegexSchema, + transformDropBlockIfContainsSchema, + transformPrependSystemBlockSchema, + transformAppendSystemBlockSchema, + transformInjectBillingHeaderSchema, +] as const; + +const transformObfuscateWordsSchema = z.object({ + kind: z.literal("obfuscate_words"), + words: z.array(z.string().max(100)).max(200), + targets: z + .array(z.enum(["system", "messages", "tools"])) + .max(3) + .optional(), +}); + export const updateSettingsSchema = z.object({ newPassword: z.string().min(1).max(200).optional(), currentPassword: z.string().max(200).optional(), @@ -131,53 +205,7 @@ export const updateSettingsSchema = z.object({ .object({ enabled: z.boolean(), pipeline: z - .array( - z.discriminatedUnion("kind", [ - z.object({ - kind: z.literal("drop_paragraph_if_contains"), - needles: z.array(z.string().max(500)).max(50), - caseSensitive: z.boolean().optional(), - }), - z.object({ - kind: z.literal("drop_paragraph_if_starts_with"), - prefixes: z.array(z.string().max(500)).max(50), - caseSensitive: z.boolean().optional(), - }), - z.object({ - kind: z.literal("replace_text"), - match: z.string().min(1).max(500), - replacement: z.string().max(500), - allOccurrences: z.boolean().optional(), - }), - z.object({ - kind: z.literal("replace_regex"), - pattern: z.string().min(1).max(500), - flags: z.string().max(10).optional(), - replacement: z.string().max(500), - }), - z.object({ - kind: z.literal("drop_block_if_contains"), - needles: z.array(z.string().max(500)).max(50), - }), - z.object({ - kind: z.literal("prepend_system_block"), - text: z.string().min(1).max(2000), - idempotencyKey: z.string().max(100).optional(), - }), - z.object({ - kind: z.literal("append_system_block"), - text: z.string().min(1).max(2000), - idempotencyKey: z.string().max(100).optional(), - }), - z.object({ - kind: z.literal("inject_billing_header"), - entrypoint: z.string().min(1).max(50), - versionFormat: z.enum(["ex-machina", "omniroute-daystamp"]), - cchAlgo: z.enum(["sha256-first-user", "xxhash64-body", "static-zero"]), - version: z.string().max(50).optional(), - }), - ]) - ) + .array(z.discriminatedUnion("kind", commonSystemTransformOperationSchemas)) .max(50), }) .optional(), @@ -193,57 +221,8 @@ export const updateSettingsSchema = z.object({ pipeline: z .array( z.discriminatedUnion("kind", [ - z.object({ - kind: z.literal("drop_paragraph_if_contains"), - needles: z.array(z.string().max(500)).max(50), - caseSensitive: z.boolean().optional(), - }), - z.object({ - kind: z.literal("drop_paragraph_if_starts_with"), - prefixes: z.array(z.string().max(500)).max(50), - caseSensitive: z.boolean().optional(), - }), - z.object({ - kind: z.literal("replace_text"), - match: z.string().min(1).max(500), - replacement: z.string().max(500), - allOccurrences: z.boolean().optional(), - }), - z.object({ - kind: z.literal("replace_regex"), - pattern: z.string().min(1).max(500), - flags: z.string().max(10).optional(), - replacement: z.string().max(500), - }), - z.object({ - kind: z.literal("drop_block_if_contains"), - needles: z.array(z.string().max(500)).max(50), - }), - z.object({ - kind: z.literal("prepend_system_block"), - text: z.string().min(1).max(2000), - idempotencyKey: z.string().max(100).optional(), - }), - z.object({ - kind: z.literal("append_system_block"), - text: z.string().min(1).max(2000), - idempotencyKey: z.string().max(100).optional(), - }), - z.object({ - kind: z.literal("inject_billing_header"), - entrypoint: z.string().min(1).max(50), - versionFormat: z.enum(["ex-machina", "omniroute-daystamp"]), - cchAlgo: z.enum(["sha256-first-user", "xxhash64-body", "static-zero"]), - version: z.string().max(50).optional(), - }), - z.object({ - kind: z.literal("obfuscate_words"), - words: z.array(z.string().max(100)).max(200), - targets: z - .array(z.enum(["system", "messages", "tools"])) - .max(3) - .optional(), - }), + ...commonSystemTransformOperationSchemas, + transformObfuscateWordsSchema, ]) ) .max(50), diff --git a/src/sse/services/model.ts b/src/sse/services/model.ts index 953c4cd09a..a587d6a556 100644 --- a/src/sse/services/model.ts +++ b/src/sse/services/model.ts @@ -8,12 +8,7 @@ import { getCustomModels, } from "@/lib/localDb"; import { getCachedSettings } from "@/lib/localDb"; -import { getComboStepTarget } from "@/lib/combos/steps"; -import { - parseModel, - resolveModelAliasFromMap, - getModelInfoCore, -} from "@omniroute/open-sse/services/model.ts"; +import { parseModel, getModelInfoCore } from "@omniroute/open-sse/services/model.ts"; import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts"; export { parseModel }; @@ -67,14 +62,6 @@ async function getCombinedModelAliases(): Promise> { return { ...dbAliases, ...settingsAliases }; } -/** - * Resolve model alias from localDb - */ -export async function resolveModelAlias(alias) { - const aliases = await getModelAliases(); - return resolveModelAliasFromMap(alias, aliases); -} - /** * Look up custom-model metadata from the DB in a single read: * - apiFormat: "responses" when the model is configured for the Responses API. @@ -265,15 +252,3 @@ export async function getComboForModel(modelStr) { return null; } - -/** - * Legacy: get combo models as string array - * @returns {Promise} - */ -export async function getComboModels(modelStr) { - const combo = await getCombo(modelStr); - if (!combo) return null; - return (combo.models || []) - .map((entry) => getComboStepTarget(entry)) - .filter((entry): entry is string => typeof entry === "string" && entry.length > 0); -} diff --git a/src/sse/utils/logger.ts b/src/sse/utils/logger.ts index 4f4617ca9e..206c92f448 100644 --- a/src/sse/utils/logger.ts +++ b/src/sse/utils/logger.ts @@ -4,7 +4,7 @@ * * Migrated from direct console logging to structured Pino logging. */ -import { createLogger, logger as rootLogger } from "@/shared/utils/logger"; +import { createLogger } from "@/shared/utils/logger"; const log = createLogger("sse"); @@ -28,18 +28,6 @@ export function request(method: string, path: string, extra?: unknown) { log.info({ tag: "HTTP", method, path, ...spreadData(extra) }, `📥 ${method} ${path}`); } -export function response(status: number, duration: number, extra?: unknown) { - const level = status < 400 ? "info" : "error"; - log[level]( - { tag: "HTTP", status, duration, ...spreadData(extra) }, - `📤 ${status} (${duration}ms)` - ); -} - -export function stream(event: string, data?: unknown) { - log.debug({ tag: "STREAM", event, ...spreadData(data) }, `🌊 ${event}`); -} - // Helper to spread data into structured fields function spreadData(data: unknown): Record { if (!data) return {}; diff --git a/src/types/apiKey.ts b/src/types/apiKey.ts deleted file mode 100644 index 01f09a0294..0000000000 --- a/src/types/apiKey.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * API key — authentication credential for accessing the OmniRoute proxy. - */ -export interface ApiKey { - id: string; - key: string; - label: string; - isActive: boolean; - createdAt: string; - lastUsedAt?: string; - usageCount: number; -} diff --git a/src/types/combo.ts b/src/types/combo.ts deleted file mode 100644 index b5befd8e57..0000000000 --- a/src/types/combo.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Combo — a routing group that distributes requests across provider nodes. - */ -export interface Combo { - id: string; - name: string; - model: string; - strategy: ComboStrategy; - isActive: boolean; - nodes: ComboNode[]; - maxRetries: number; - retryDelayMs: number; - fallbackDelayMs?: number; - timeoutMs: number; - healthCheckEnabled: boolean; - createdAt: string; - updatedAt: string; -} - -export type ComboStrategy = "priority" | "weighted" | "round-robin" | "context-relay"; - -export interface ComboNode { - connectionId: string; - provider: string; - weight: number; - priority: number; -} diff --git a/src/types/index.ts b/src/types/index.ts index ba28c26d70..516413be7d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,10 +5,6 @@ * Import from "@/types" in any file. */ -export type { ProviderConnection, ProviderNode, ModelCooldownErrorPayload } from "./provider"; -export type { ApiKey } from "./apiKey"; -export type { Combo, ComboStrategy, ComboNode } from "./combo"; -export type { UsageEntry, UsageStats, ProviderUsageStats, ModelUsageStats, CallLog } from "./usage"; -export type { Settings, ComboDefaults, ProxyConfig, KVPair } from "./settings"; +export type { ModelCooldownErrorPayload } from "./provider"; export type { DatabaseSettings } from "./databaseSettings"; export { DEFAULT_DATABASE_SETTINGS } from "./databaseSettings"; diff --git a/src/types/provider.ts b/src/types/provider.ts index 2647a51123..051b1d780c 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -1,34 +1,3 @@ -/** - * Provider connection — a configured cloud AI provider account. - */ -export interface ProviderConnection { - id: string; - provider: string; - label: string; - url: string; - apiKey?: string; - oauthToken?: string; - oauthRefreshToken?: string; - oauthExpiresAt?: string; - isActive: boolean; - priority: number; - createdAt: string; - updatedAt: string; -} - -/** - * Provider node — a specific endpoint within a provider. - */ -export interface ProviderNode { - id: string; - connectionId: string; - provider: string; - model: string; - baseUrl: string; - isActive: boolean; - priority: number; -} - export interface ModelCooldownErrorPayload { error: { message: string; diff --git a/src/types/settings.ts b/src/types/settings.ts deleted file mode 100644 index 1cac06c988..0000000000 --- a/src/types/settings.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { HideableSidebarGroupId } from "@/shared/constants/sidebarGroupVisibility"; -import type { - HideableSidebarItemId, - SidebarItemOrder, - SidebarPresetId, - SidebarSectionId, -} from "@/shared/constants/sidebarVisibility"; -import type { ResilienceSettings } from "@/lib/resilience/settings"; -import type { - AccountFallbackStrategyValue, - RoutingStrategyValue, -} from "@/shared/constants/routingStrategies"; - -/** - * Application settings stored in SQLite key-value pairs. - */ -export interface Settings { - requireLogin: boolean; - hasPassword: boolean; - fallbackStrategy: AccountFallbackStrategyValue; - stickyRoundRobinLimit: number; - requestRetry: number; - maxRetryIntervalSec: number; - maxBodySizeMb?: number; - jwtSecret?: string; - mcpEnabled?: boolean; - mcpTransport?: "stdio" | "sse" | "streamable-http"; - a2aEnabled?: boolean; - hideHealthCheckLogs?: boolean; - hideEndpointCloudflaredTunnel?: boolean; - hideEndpointTailscaleFunnel?: boolean; - hideEndpointNgrokTunnel?: boolean; - preferClaudeCodeForUnprefixedClaudeModels?: boolean; - autoRefreshProviderQuota?: boolean; - autoRefreshProviderQuotaInterval?: number; - pinProviderQuotaToHome?: boolean; - showQuickStartOnHome?: boolean; - showProviderTopologyOnHome?: boolean; - hiddenSidebarItems?: HideableSidebarItemId[]; - hiddenSidebarGroupLabels?: HideableSidebarGroupId[]; - sidebarSectionOrder?: SidebarSectionId[]; - sidebarItemOrder?: SidebarItemOrder; - sidebarActivePreset?: SidebarPresetId; - resilienceSettings?: ResilienceSettings; - // LOCAL_ONLY manage-scope bypass policy (DB-stored, hot-reloaded by - // `applyRuntimeSettings` → `applyAuthzBypassSection`). The route guard - // consults `getAuthzBypassSnapshot()` on the hot path; these fields are - // the persisted source of truth that feeds that snapshot. - localOnlyManageScopeBypassEnabled?: boolean; - localOnlyManageScopeBypassPrefixes?: string[]; -} - -export interface ComboDefaults { - strategy: RoutingStrategyValue; - maxRetries: number; - retryDelayMs: number; - fallbackDelayMs?: number; - maxComboDepth: number; - trackMetrics: boolean; - reasoningTokenBufferEnabled?: boolean; - concurrencyPerModel?: number; - queueTimeoutMs?: number; - queueDepth?: number; - handoffThreshold?: number; - handoffModel?: string; - handoffProviders?: string[]; - maxMessagesForSummary?: number; -} - -export interface ProxyConfig { - type: "http" | "https" | "socks5"; - host: string; - port: number; - username?: string; - password?: string; -} - -export interface KVPair { - key: string; - value: string; -} diff --git a/src/types/usage.ts b/src/types/usage.ts deleted file mode 100644 index 4fe7ebf879..0000000000 --- a/src/types/usage.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Usage tracking types for API call monitoring and statistics. - */ -export interface UsageEntry { - id: string; - date: string; - provider: string; - model: string; - inputTokens: number; - outputTokens: number; - totalTokens: number; - latencyMs: number; - statusCode: number; - comboId?: string; - apiKeyId?: string; -} - -export interface UsageStats { - totalCalls: number; - totalTokens: number; - totalInputTokens: number; - totalOutputTokens: number; - avgLatencyMs: number; - errorRate: number; - byProvider: Record; - byModel: Record; -} - -export interface ProviderUsageStats { - calls: number; - tokens: number; - errors: number; - avgLatencyMs: number; -} - -export interface ModelUsageStats { - calls: number; - inputTokens: number; - outputTokens: number; - avgLatencyMs: number; -} - -export interface CallLog { - id: string; - timestamp: string; - method: string; - path: string; - provider: string; - model: string; - statusCode: number; - latencyMs: number; - inputTokens: number; - outputTokens: number; - error?: string; - comboId?: string; - connectionId?: string; -} diff --git a/tests/integration/integration-wiring.test.ts b/tests/integration/integration-wiring.test.ts index acad6b66c6..348b41c057 100644 --- a/tests/integration/integration-wiring.test.ts +++ b/tests/integration/integration-wiring.test.ts @@ -602,18 +602,15 @@ describe("Page Integration — provider test results privacy", () => { }); }); -describe("Page Integration — legacy provider create route retirement", () => { - const legacyProviderNewSrc = readProjectFile( - "src/app/(dashboard)/dashboard/providers/new/page.tsx" - ); +describe("Page Integration — provider create route renders the onboarding wizard (#5427)", () => { + const providerNewSrc = readProjectFile("src/app/(dashboard)/dashboard/providers/new/page.tsx"); - it("should redirect legacy /dashboard/providers/new to the canonical providers flow", () => { - assert.ok( - legacyProviderNewSrc, - "src/app/(dashboard)/dashboard/providers/new/page.tsx should exist" - ); - assert.match(legacyProviderNewSrc, /redirect\("\/dashboard\/providers"\)/); - assert.doesNotMatch(legacyProviderNewSrc, /authMethod:\s*"api_key"/); - assert.doesNotMatch(legacyProviderNewSrc, /displayName/); + it("renders ProviderOnboardingWizard instead of redirecting (#5427)", () => { + // #5427 reversed the earlier redirect-stub retirement: /dashboard/providers/new now + // renders the previously-orphaned ProviderOnboardingWizard directly (auth enforced by + // the (dashboard) layout). The dedicated guard is tests/unit/onboarding-wizard-route-5427. + assert.ok(providerNewSrc, "src/app/(dashboard)/dashboard/providers/new/page.tsx should exist"); + assert.match(providerNewSrc, /ProviderOnboardingWizard/); + assert.doesNotMatch(providerNewSrc, /redirect\("\/dashboard\/providers"\)/); }); }); diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index e1952f24d3..9467e78189 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -538,8 +538,8 @@ } }, "url": { - "nonStream": "https://api.bytez.com/models/v2", - "stream": "https://api.bytez.com/models/v2" + "nonStream": "https://api.bytez.com/models/v2/openai/v1/chat/completions", + "stream": "https://api.bytez.com/models/v2/openai/v1/chat/completions" } }, "cerebras": { @@ -1451,8 +1451,8 @@ } }, "url": { - "nonStream": "https://api.friendli.ai/dedicated/v1/chat/completions", - "stream": "https://api.friendli.ai/dedicated/v1/chat/completions" + "nonStream": "https://api.friendli.ai/serverless/v1/chat/completions", + "stream": "https://api.friendli.ai/serverless/v1/chat/completions" } }, "galadriel": { @@ -2860,8 +2860,8 @@ } }, "url": { - "nonStream": "https://api.novita.ai/v3/chat/completions", - "stream": "https://api.novita.ai/v3/chat/completions" + "nonStream": "https://api.novita.ai/openai/v1/chat/completions", + "stream": "https://api.novita.ai/openai/v1/chat/completions" } }, "nscale": { @@ -3175,29 +3175,6 @@ "stream": "https://www.perplexity.ai/rest/sse/perplexity_ask" } }, - "phind": { - "format": "openai", - "headers": { - "apiKey": { - "Accept": "text/event-stream", - "Authorization": "Bearer ", - "Content-Type": "application/json" - }, - "nonStream": { - "Authorization": "Bearer ", - "Content-Type": "application/json" - }, - "oauth": { - "Accept": "text/event-stream", - "Authorization": "Bearer ", - "Content-Type": "application/json" - } - }, - "url": { - "nonStream": "https://www.phind.com/api/chat", - "stream": "https://www.phind.com/api/chat" - } - }, "pioneer": { "format": "openai", "headers": { diff --git a/tests/unit/_mocks/settings.ts b/tests/unit/_mocks/settings.ts index 535f28cdf6..2ccd31da52 100644 --- a/tests/unit/_mocks/settings.ts +++ b/tests/unit/_mocks/settings.ts @@ -17,7 +17,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import type { Settings } from "../../../src/types/settings"; + +type SettingsPatch = Record; let activeFixture: SettingsFixture | null = null; @@ -61,9 +62,9 @@ export function setupSettingsFixture(slug: string): SettingsFixture { * `updateSettings → applyRuntimeSettings` pipeline. Hot-reload side effects * (route guard snapshot, etc.) fire exactly as they do in `PATCH /api/settings`. */ -export async function mockSettings(partial: Partial): Promise> { +export async function mockSettings(partial: SettingsPatch): Promise> { const settingsDb = await import("../../../src/lib/db/settings.ts"); - return settingsDb.updateSettings(partial as Record); + return settingsDb.updateSettings(partial); } /** diff --git a/tests/unit/a2a-routing-logger.test.ts b/tests/unit/a2a-routing-logger.test.ts new file mode 100644 index 0000000000..03b83e1927 --- /dev/null +++ b/tests/unit/a2a-routing-logger.test.ts @@ -0,0 +1,32 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { logRoutingDecision } from "../../src/lib/a2a/routingLogger.ts"; + +test("logRoutingDecision records a routing decision with generated metadata", () => { + const decision = logRoutingDecision({ + taskType: "chat", + comboId: "combo-1", + providerSelected: "openai", + modelUsed: "gpt-4o-mini", + score: 0.91, + factors: [ + { + name: "health", + value: 1, + weight: 0.5, + contribution: 0.5, + }, + ], + fallbacksTriggered: [], + success: true, + latencyMs: 123, + cost: 0.001, + }); + + assert.equal(typeof decision.requestId, "string"); + assert.match(decision.requestId, /^[0-9a-f-]{36}$/); + assert.match(decision.timestamp, /^\d{4}-\d{2}-\d{2}T/); + assert.equal(decision.providerSelected, "openai"); + assert.equal(decision.factors[0].name, "health"); +}); diff --git a/tests/unit/agentSkills-schemas.test.ts b/tests/unit/agentSkills-schemas.test.ts index 7172f3e7aa..7d6cf38fc8 100644 --- a/tests/unit/agentSkills-schemas.test.ts +++ b/tests/unit/agentSkills-schemas.test.ts @@ -4,6 +4,20 @@ import assert from "node:assert/strict"; const { AgentSkillSchema, SkillCoverageSchema, ListQuerySchema, GenerateBodySchema } = await import("../../src/lib/agentSkills/schemas.ts"); +test("agent skills schemas module exposes runtime validators", async () => { + const schemas = await import("../../src/lib/agentSkills/schemas.ts"); + + for (const key of [ + "AgentSkillSchema", + "GenerateBodySchema", + "ListQuerySchema", + "SkillCategorySchema", + "SkillCoverageSchema", + ]) { + assert.equal(typeof schemas[key].safeParse, "function", `${key} should stay exported`); + } +}); + // ─── AgentSkillSchema ───────────────────────────────────────────────────────── test("AgentSkillSchema — valid api skill parses successfully", () => { @@ -105,7 +119,15 @@ test("AgentSkillSchema — optional fields absent parses successfully", () => { test("AgentSkillSchema — .parse throws on invalid input", () => { assert.throws(() => { - AgentSkillSchema.parse({ id: "bad id", name: "", description: "", category: "api", area: "x", rawUrl: "x", githubUrl: "x" }); + AgentSkillSchema.parse({ + id: "bad id", + name: "", + description: "", + category: "api", + area: "x", + rawUrl: "x", + githubUrl: "x", + }); }); }); diff --git a/tests/unit/api/v1/bifrost-cooldown.test.ts b/tests/unit/api/v1/bifrost-cooldown.test.ts new file mode 100644 index 0000000000..7d3bb193d4 --- /dev/null +++ b/tests/unit/api/v1/bifrost-cooldown.test.ts @@ -0,0 +1,45 @@ +import { afterEach, test } from "node:test"; +import assert from "node:assert/strict"; +import { + clearBifrostFailure, + getActiveBifrostCooldown, + getBifrostFailureCooldownMs, + recordBifrostFailure, + resetBifrostCooldowns, +} from "../../../../src/app/api/v1/relay/chat/completions/bifrostCooldown.ts"; + +afterEach(() => { + resetBifrostCooldowns(); +}); + +test("bifrost cooldown defaults to a short retry suppression window", () => { + assert.equal(getBifrostFailureCooldownMs({}), 5000); + assert.equal( + getBifrostFailureCooldownMs({ OMNIROUTE_BIFROST_FAILURE_COOLDOWN_MS: "250" }), + 250 + ); + assert.equal( + getBifrostFailureCooldownMs({ OMNIROUTE_BIFROST_FAILURE_COOLDOWN_MS: "bad" }), + 5000 + ); +}); + +test("bifrost cooldown reports remaining time and expires", () => { + recordBifrostFailure("http://bifrost.local", "timeout", 1000, 500); + + assert.deepEqual(getActiveBifrostCooldown("http://bifrost.local", 1100), { + remainingMs: 400, + reason: "timeout", + }); + assert.equal(getActiveBifrostCooldown("http://bifrost.local", 1501), null); +}); + +test("bifrost cooldown can be disabled or cleared", () => { + recordBifrostFailure("http://bifrost.local", "timeout", 1000, 0); + assert.equal(getActiveBifrostCooldown("http://bifrost.local", 1001), null); + + recordBifrostFailure("http://bifrost.local", "timeout", 1000, 500); + clearBifrostFailure("http://bifrost.local"); + + assert.equal(getActiveBifrostCooldown("http://bifrost.local", 1001), null); +}); diff --git a/tests/unit/api/v1/relay-routing-backend.test.ts b/tests/unit/api/v1/relay-routing-backend.test.ts index 1545fbb4f9..c30af34b20 100644 --- a/tests/unit/api/v1/relay-routing-backend.test.ts +++ b/tests/unit/api/v1/relay-routing-backend.test.ts @@ -2,6 +2,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { getBifrostRoutingConfig, + getRoutingFallbackHeader, resolveRelayRoutingBackend, shouldTryBifrost, } from "../../../../src/app/api/v1/relay/chat/completions/routingBackend.ts"; @@ -66,3 +67,34 @@ test("relay routing backend falls back on invalid timeout values", () => { assert.equal(getBifrostRoutingConfig(env)?.timeoutMs, 30000); }); + +test("relay routing backend exposes TS fallback header only for enabled auto bifrost fallback", () => { + const config = getBifrostRoutingConfig({ + BIFROST_BASE_URL: "http://127.0.0.1:8080", + }); + + assert.equal(getRoutingFallbackHeader("auto", config), "bifrost"); + assert.equal(getRoutingFallbackHeader("ts", config), undefined); + assert.equal(getRoutingFallbackHeader("bifrost", config), undefined); + assert.equal( + getRoutingFallbackHeader( + "auto", + getBifrostRoutingConfig({ + BIFROST_BASE_URL: "http://127.0.0.1:8080", + BIFROST_ENABLED: "0", + }) + ), + undefined + ); + assert.equal(getRoutingFallbackHeader("auto", null), undefined); +}); + +test("relay routing backend keeps strict bifrost failures out of auto fallback accounting", () => { + const config = getBifrostRoutingConfig({ + BIFROST_BASE_URL: "http://127.0.0.1:8080", + }); + + assert.equal(shouldTryBifrost("bifrost", config), true); + assert.equal(getRoutingFallbackHeader("bifrost", config), undefined); + assert.equal(resolveRelayRoutingBackend({ OMNIROUTE_RELAY_BACKEND: "bifrost" }), "bifrost"); +}); diff --git a/tests/unit/api/v1/relay-stream-finalizer.test.ts b/tests/unit/api/v1/relay-stream-finalizer.test.ts new file mode 100644 index 0000000000..3be9fbeb14 --- /dev/null +++ b/tests/unit/api/v1/relay-stream-finalizer.test.ts @@ -0,0 +1,46 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { finalizeReadableStream } from "../../../../src/app/api/v1/relay/chat/completions/streamFinalizer.ts"; + +test("finalizeReadableStream finalizes once after the wrapped stream completes", async () => { + const finalized: unknown[] = []; + const stream = finalizeReadableStream( + new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("hello")); + controller.close(); + }, + }), + (error) => finalized.push(error) + ); + + assert.equal(await new Response(stream).text(), "hello"); + assert.deepEqual(finalized, [undefined]); +}); + +test("finalizeReadableStream finalizes once when the consumer cancels", async () => { + const finalized: unknown[] = []; + let cancelReason: unknown; + const stream = finalizeReadableStream( + new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("chunk")); + }, + cancel(reason) { + cancelReason = reason; + }, + }), + (error) => finalized.push(error) + ); + + const reader = stream.getReader(); + const first = await reader.read(); + assert.equal(new TextDecoder().decode(first.value), "chunk"); + + await reader.cancel("client disconnected"); + await reader.cancel("second cancel"); + + assert.equal(cancelReason, "client disconnected"); + assert.deepEqual(finalized, ["client disconnected"]); +}); diff --git a/tests/unit/audit-high-level-actions.test.ts b/tests/unit/audit-high-level-actions.test.ts index a3fc1f7cf4..e0ac0b7dba 100644 --- a/tests/unit/audit-high-level-actions.test.ts +++ b/tests/unit/audit-high-level-actions.test.ts @@ -5,6 +5,13 @@ import { HIGH_LEVEL_ACTIONS, isHighLevelAction } from "../../src/lib/audit/highL const ALL = HIGH_LEVEL_ACTIONS as readonly string[]; +test("high-level actions module exposes runtime allowlist helpers", async () => { + const mod = await import("../../src/lib/audit/highLevelActions"); + + assert.equal(Array.isArray(mod.HIGH_LEVEL_ACTIONS), true); + assert.equal(typeof mod.isHighLevelAction, "function"); +}); + // B/G3: allowlist aligned with logAuditEvent emitters (27 after batch_updated). test("HIGH_LEVEL_ACTIONS has exactly 27 entries", () => { assert.equal(ALL.length, 27); diff --git a/tests/unit/authz/csrf.test.ts b/tests/unit/authz/csrf.test.ts new file mode 100644 index 0000000000..eff1120e42 --- /dev/null +++ b/tests/unit/authz/csrf.test.ts @@ -0,0 +1,86 @@ +import assert from "node:assert/strict"; +import { describe, it, beforeEach, after } from "node:test"; + +import { DASHBOARD_CSRF_HEADER } from "@/shared/constants/dashboardCsrf"; +import { issueDashboardCsrfToken, validateDashboardCsrfToken } from "@/server/authz/csrf"; + +const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET; + +beforeEach(() => { + process.env.JWT_SECRET = "csrf-test-secret"; +}); + +after(() => { + if (ORIGINAL_JWT_SECRET === undefined) delete process.env.JWT_SECRET; + else process.env.JWT_SECRET = ORIGINAL_JWT_SECRET; +}); + +function request(path: string, cookie = "auth_token=session-a", token?: string): Request { + return new Request(`http://127.0.0.1:20128${path}`, { + method: "POST", + headers: { + cookie, + ...(token ? { [DASHBOARD_CSRF_HEADER]: token } : {}), + }, + }); +} + +describe("dashboard CSRF tokens", () => { + it("accepts a valid token for dashboard test mutation paths", () => { + const issued = issueDashboardCsrfToken(request("/api/auth/csrf"), 1_000); + + assert.ok(issued); + assert.equal( + validateDashboardCsrfToken(request("/api/models/test", undefined, issued.token), 1_000), + true + ); + assert.equal( + validateDashboardCsrfToken(request("/api/models/test-all", undefined, issued.token), 1_000), + true + ); + assert.equal( + validateDashboardCsrfToken(request("/api/combos/test", undefined, issued.token), 1_000), + true + ); + }); + + it("rejects tokens on non-test management paths", () => { + const issued = issueDashboardCsrfToken(request("/api/auth/csrf"), 1_000); + + assert.ok(issued); + assert.equal( + validateDashboardCsrfToken(request("/api/keys", undefined, issued.token), 1_000), + false + ); + }); + + it("binds tokens to the dashboard auth cookie", () => { + const issued = issueDashboardCsrfToken( + request("/api/auth/csrf", "auth_token=session-a"), + 1_000 + ); + + assert.ok(issued); + assert.equal( + validateDashboardCsrfToken( + request("/api/models/test", "auth_token=session-b", issued.token), + 1_000 + ), + false + ); + }); + + it("rejects expired and tampered tokens", () => { + const issued = issueDashboardCsrfToken(request("/api/auth/csrf"), 1_000); + + assert.ok(issued); + assert.equal( + validateDashboardCsrfToken(request("/api/models/test", undefined, issued.token), 700_000), + false + ); + assert.equal( + validateDashboardCsrfToken(request("/api/models/test", undefined, `${issued.token}x`), 1_000), + false + ); + }); +}); diff --git a/tests/unit/authz/pipeline.test.ts b/tests/unit/authz/pipeline.test.ts index ab740146da..f2c0dbae5e 100644 --- a/tests/unit/authz/pipeline.test.ts +++ b/tests/unit/authz/pipeline.test.ts @@ -14,6 +14,8 @@ const core = await import("../../../src/lib/db/core.ts"); const apiKeysDb = await import("../../../src/lib/db/apiKeys.ts"); const settingsDb = await import("../../../src/lib/db/settings.ts"); const pipeline = await import("../../../src/server/authz/pipeline.ts"); +const csrf = await import("../../../src/server/authz/csrf.ts"); +const dashboardCsrfConstants = await import("../../../src/shared/constants/dashboardCsrf.ts"); const ORIGINAL_JWT = process.env.JWT_SECRET; const ORIGINAL_INITIAL = process.env.INITIAL_PASSWORD; @@ -323,6 +325,94 @@ test("runAuthzPipeline accepts dashboard mutations from configured public origin assert.equal(response.headers.get("x-omniroute-route-class"), "MANAGEMENT"); }); +test("runAuthzPipeline rejects dashboard test mutations from dynamic public origins without CSRF", async () => { + await forceAuthRequired(); + + const response = await pipeline.runAuthzPipeline( + request("http://127.0.0.1:20128/api/models/test", { + method: "POST", + headers: { + cookie: await dashboardCookie(), + host: "127.0.0.1:20128", + origin: "https://random-tunnel.example.test", + "content-type": "application/json", + "sec-fetch-site": "same-origin", + }, + body: "{}", + }), + { enforce: true } + ); + const body = await response.json(); + + assert.equal(response.status, 403); + assert.equal(body.error.code, "INVALID_ORIGIN"); +}); + +test("runAuthzPipeline accepts dashboard test mutations from dynamic public origins with CSRF", async () => { + await forceAuthRequired(); + + const cookie = await dashboardCookie(); + const issued = csrf.issueDashboardCsrfToken( + request("http://127.0.0.1:20128/api/auth/csrf", { + headers: { cookie }, + }) + ); + assert.ok(issued); + + for (const path of ["/api/combos/test", "/api/models/test", "/api/models/test-all"]) { + const response = await pipeline.runAuthzPipeline( + request(`http://127.0.0.1:20128${path}`, { + method: "POST", + headers: { + cookie, + host: "127.0.0.1:20128", + origin: "https://random-tunnel.example.test", + "content-type": "application/json", + [dashboardCsrfConstants.DASHBOARD_CSRF_HEADER]: issued.token, + "sec-fetch-site": "same-origin", + }, + body: "{}", + }), + { enforce: true } + ); + + assert.equal(response.status, 200, path); + assert.equal(response.headers.get("x-omniroute-route-class"), "MANAGEMENT"); + } +}); + +test("runAuthzPipeline keeps non-test management mutations pinned to known origins with CSRF", async () => { + await forceAuthRequired(); + + const cookie = await dashboardCookie(); + const issued = csrf.issueDashboardCsrfToken( + request("http://127.0.0.1:20128/api/auth/csrf", { + headers: { cookie }, + }) + ); + assert.ok(issued); + + const response = await pipeline.runAuthzPipeline( + request("http://127.0.0.1:20128/api/keys", { + method: "POST", + headers: { + cookie, + host: "127.0.0.1:20128", + origin: "https://random-tunnel.example.test", + "content-type": "application/json", + [dashboardCsrfConstants.DASHBOARD_CSRF_HEADER]: issued.token, + "sec-fetch-site": "same-origin", + }, + body: "{}", + }), + { enforce: true } + ); + const body = await response.json(); + + assert.equal(response.status, 403); + assert.equal(body.error.code, "INVALID_ORIGIN"); +}); + test("runAuthzPipeline rejects dashboard mutations from invalid browser origin", async () => { await forceAuthRequired(); process.env.NEXT_PUBLIC_BASE_URL = "https://gateway.example.test"; diff --git a/tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts b/tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts new file mode 100644 index 0000000000..aeeb98bbdf --- /dev/null +++ b/tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts @@ -0,0 +1,89 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { SPAWN_CAPABLE_PREFIXES } from "@/shared/constants/spawnCapablePrefixes"; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); +const VALIDATION_DIR = join(ROOT, "src/shared/validation"); + +function walkTsFiles(dir: string): string[] { + const out: string[] = []; + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + if (statSync(full).isDirectory()) out.push(...walkTsFiles(full)); + else if (/\.tsx?$/.test(entry) && !/\.test\.tsx?$/.test(entry)) out.push(full); + } + return out; +} + +/** + * Returns the VALUE-import specifiers in `src` (i.e. imports that survive to the + * runtime bundle). `import type …` is excluded — it is erased by the compiler/SWC and + * never creates a webpack bundle edge, so it cannot leak a Node-only dep into a + * client bundle. + */ +function valueImportSpecifiers(src: string): string[] { + const specs: string[] = []; + // import [type] [ from] "" — `[^"';]*?` spans multi-line clauses. + const re = /\bimport\s+(type\s+)?(?:[^"';]*?\bfrom\s*)?["']([^"']+)["']/g; + let m: RegExpExecArray | null; + while ((m = re.exec(src))) { + if (m[1]) continue; // `import type …` — erased, not a runtime edge + specs.push(m[2]); + } + return specs; +} + +// Regression guard for the dast-smoke "Build CLI bundle" failure: +// Module not found: Can't resolve 'dns' / 'net' (./node_modules/ioredis/...) +// Root cause: `settingsSchemas.ts` VALUE-imported SPAWN_CAPABLE_PREFIXES from +// `@/server/authz/routeGuard`, whose server runtime (runtimeSettings → localDb → +// apiKeys → rateLimiter → ioredis) then got dragged into the client/CLI webpack bundle +// via the dashboard onboarding wizard → validation barrel chain. Validation schemas are +// client-reachable and MUST depend only on zod + `@/shared` leaves — never on the +// server (`@/server/…`) or server-side lib (`@/lib/…`, which reaches the DB/ioredis). +const FORBIDDEN_VALUE_ROOTS = ["@/server/", "@/lib/"]; + +test("validation schemas must not VALUE-import from server-side roots (client/CLI build safety)", () => { + const offenders: string[] = []; + for (const file of walkTsFiles(VALIDATION_DIR)) { + const rel = file.slice(ROOT.length + 1); + for (const spec of valueImportSpecifiers(readFileSync(file, "utf8"))) { + if (FORBIDDEN_VALUE_ROOTS.some((root) => spec.startsWith(root))) { + offenders.push(`${rel} → ${spec}`); + } + } + } + assert.deepEqual( + offenders, + [], + "Client-reachable validation schemas VALUE-import server-side modules, which drags the " + + "server runtime (→ ioredis) into the browser/CLI bundle and breaks the Next build with " + + `"Can't resolve 'dns'/'net'". Move the needed value to a server-free @/shared/constants ` + + `leaf (see src/shared/constants/spawnCapablePrefixes.ts). Offenders:\n ${offenders.join("\n ")}` + ); +}); + +test("SPAWN_CAPABLE_PREFIXES is defined in the server-free constants leaf with the expected entries", () => { + assert.ok(Array.isArray(SPAWN_CAPABLE_PREFIXES)); + // The full deny-list survived the extraction out of routeGuard.ts (Hard Rules #15/#17). + for (const prefix of [ + "/api/cli-tools/runtime/", + "/api/services/", + "/api/tools/agent-bridge/", + "/api/tools/traffic-inspector/", + "/api/plugins/", + "/api/local/", + "/api/headroom/start", + "/api/headroom/stop", + ]) { + assert.ok( + SPAWN_CAPABLE_PREFIXES.includes(prefix), + `SPAWN_CAPABLE_PREFIXES lost the spawn-capable prefix "${prefix}" during extraction` + ); + } + assert.equal(SPAWN_CAPABLE_PREFIXES.length, 8); +}); diff --git a/tests/unit/bailian-coding-plan-provider.test.ts b/tests/unit/bailian-coding-plan-provider.test.ts index c0de35dd19..2492071a86 100644 --- a/tests/unit/bailian-coding-plan-provider.test.ts +++ b/tests/unit/bailian-coding-plan-provider.test.ts @@ -124,6 +124,28 @@ test("createProviderSchema rejects baseUrl with non-string value", () => { assert.equal(validation.success, false, "Should reject non-string baseUrl"); }); +test("createProviderSchema rejects non-boolean Codex context1m request default", () => { + const validation = validateBody(createProviderSchema, { + provider: "codex", + apiKey: "sk-test-key", + name: "Test Codex", + providerSpecificData: { + requestDefaults: { + context1m: "yes", + }, + }, + }); + + assert.equal(validation.success, false, "Should reject non-boolean context1m"); + if (!validation.success && typeof validation.error === "object" && validation.error !== null) { + const details = Array.isArray(validation.error.details) ? validation.error.details : []; + assert.ok( + details.some((detail) => String(detail.message || "").includes("context1m")), + "Error should mention context1m" + ); + } +}); + test("updateProviderConnectionSchema accepts valid baseUrl in providerSpecificData", () => { const validation = validateBody(updateProviderConnectionSchema, { providerSpecificData: { diff --git a/tests/unit/base-thinking-budget-config-5312.test.ts b/tests/unit/base-thinking-budget-config-5312.test.ts index 92578a1a8c..046a3348de 100644 --- a/tests/unit/base-thinking-budget-config-5312.test.ts +++ b/tests/unit/base-thinking-budget-config-5312.test.ts @@ -52,6 +52,11 @@ async function captureUpstreamBody( body, stream: false, credentials: { accessToken: "sk-ant-oat-test-5312" }, + // #5480: the default adaptive-thinking injection (and the native-Claude-Code wire + // image these #5312 cases exercise) is gated behind a real Claude Code client + // (`x-app: cli` / `claude-code` UA). A bare OAuth token from a generic OpenAI-compat + // client must opt in via x-omniroute-thinking, so identify as a Claude Code client here. + clientHeaders: { "x-app": "cli" }, }); } finally { globalThis.fetch = originalFetch; diff --git a/tests/unit/bytez-validation-5422.test.ts b/tests/unit/bytez-validation-5422.test.ts new file mode 100644 index 0000000000..dbf634ab09 --- /dev/null +++ b/tests/unit/bytez-validation-5422.test.ts @@ -0,0 +1,45 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { bytezValidationResultFromStatus } from "../../src/lib/providers/validation.ts"; +import { bytezProvider } from "../../open-sse/config/providers/registry/bytez/index.ts"; + +// #5422 — Bytez key validation cannot use a chat probe. A Bytez account only serves models +// that have been added to its catalog, so even Bytez's own documented model ids return 404 +// ("Model does not exist or has yet to be added to the Bytez catalog") for a fresh/free key — +// the generic OpenAI-like chat probe misreads that 404 as "endpoint not supported". The fix +// validates against the model-independent, auth-only tasks endpoint instead (verified live): +// GET …/models/v2/list/tasks → 200 (valid key) | 401 { error: "Unauthorized" } (invalid). +// The pure status→result mapping below is the unit-testable core of that validator. + +test("#5422 bytez status→result: 200 is valid", () => { + assert.deepEqual(bytezValidationResultFromStatus(200), { valid: true, error: null }); +}); + +test("#5422 bytez status→result: 401/403 is an invalid API key", () => { + assert.deepEqual(bytezValidationResultFromStatus(401), { + valid: false, + error: "Invalid API key", + }); + assert.deepEqual(bytezValidationResultFromStatus(403), { + valid: false, + error: "Invalid API key", + }); +}); + +test("#5422 bytez status→result: other non-OK is a generic validation failure", () => { + assert.deepEqual(bytezValidationResultFromStatus(500), { + valid: false, + error: "Validation failed: 500", + }); +}); + +// Part A — the registry baseUrl must carry the full OpenAI-compat chat path so chat resolves +// once an account has catalog models (the bare `…/models/v2` base made the probe hit +// `…/models/v2/chat/completions` → 404). +test("#5422 bytez registry baseUrl carries the full OpenAI-compat chat path", () => { + assert.ok( + bytezProvider.baseUrl.endsWith("/models/v2/openai/v1/chat/completions"), + `baseUrl must end with the OpenAI-compat chat path, got: ${bytezProvider.baseUrl}` + ); +}); diff --git a/tests/unit/caveman-packs-de-fr-ja.test.ts b/tests/unit/caveman-packs-de-fr-ja.test.ts new file mode 100644 index 0000000000..d0c08badb8 --- /dev/null +++ b/tests/unit/caveman-packs-de-fr-ja.test.ts @@ -0,0 +1,57 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + loadRulePack, + loadAllRulesForLanguage, +} from "../../open-sse/services/compression/ruleLoader.ts"; + +// T05 / C2 — caveman dedup + ultra packs for de, fr, ja (previously only context/filler/structural). +const LANGS = ["de", "fr", "ja"] as const; + +// Samples chosen to contain terms the ultra pack abbreviates. For ja the katakana loanwords +// (データベース/アプリケーション/…) are the clear character-length wins; the short-kanji rules +// (設定→config, 認証→auth) are token wins but can be char-neutral, so they stay out of this sample. +const ULTRA_SAMPLES: Record<(typeof LANGS)[number], string> = { + de: "Die Datenbank-Konfiguration der Anwendung benötigt eine Authentifizierung.", + fr: "La configuration de la base de données de l'application nécessite une authentification.", + ja: "データベースとアプリケーション、レスポンスとリクエストとコンフィギュレーション", +}; + +type LoadedRule = ReturnType[number]; + +function applyAll(text: string, rules: LoadedRule[]): string { + return rules.reduce((acc, rule) => { + return typeof rule.replacement === "function" + ? acc.replace(rule.pattern, rule.replacement) + : acc.replace(rule.pattern, rule.replacement); + }, text); +} + +for (const lang of LANGS) { + test(`caveman ${lang}: dedup + ultra packs load and validate`, () => { + // loadRulePack throws if the pack fails validateRulePack, so a non-throwing non-empty load + // proves both presence and schema validity. + const dedup = loadRulePack(lang, "dedup", { refresh: true }); + const ultra = loadRulePack(lang, "ultra", { refresh: true }); + assert.ok(dedup.length > 0, `${lang}/dedup should have rules`); + assert.ok(ultra.length > 0, `${lang}/ultra should have rules`); + }); + + test(`caveman ${lang}: ultra pack shrinks a representative sample`, () => { + const ultra = loadRulePack(lang, "ultra", { refresh: true }); + const sample = ULTRA_SAMPLES[lang]; + const out = applyAll(sample, ultra); + assert.ok( + out.length < sample.length, + `${lang} ultra should shrink the sample (${sample.length} -> ${out.length})` + ); + }); +} + +test("de/fr/ja expose dedup + ultra categories via loadAllRulesForLanguage", () => { + for (const lang of LANGS) { + const cats = new Set(loadAllRulesForLanguage(lang, { refresh: true }).map((r) => r.category)); + assert.ok(cats.has("dedup"), `${lang} should expose the dedup category`); + assert.ok(cats.has("ultra"), `${lang} should expose the ultra category`); + } +}); diff --git a/tests/unit/caveman-packs-zh-wenyan.test.ts b/tests/unit/caveman-packs-zh-wenyan.test.ts new file mode 100644 index 0000000000..cf22be97b9 --- /dev/null +++ b/tests/unit/caveman-packs-zh-wenyan.test.ts @@ -0,0 +1,69 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + loadRulePack, + loadAllRulesForLanguage, +} from "../../open-sse/services/compression/ruleLoader.ts"; +import { + detectCompressionLanguage, + listSupportedCompressionLanguages, +} from "../../open-sse/services/compression/languageDetector.ts"; + +// T05 / C6 — Chinese (zh / wenyan) caveman pack: the input-side counterpart of the existing +// output-side "terse-cjk" (文言) style. Adds dedup + filler + ultra rule packs and zh detection. + +type LoadedRule = ReturnType[number]; + +function applyAll(text: string, rules: LoadedRule[]): string { + return rules.reduce((acc, rule) => { + return typeof rule.replacement === "function" + ? acc.replace(rule.pattern, rule.replacement) + : acc.replace(rule.pattern, rule.replacement); + }, text); +} + +test("zh: dedup + filler + ultra packs load and validate", () => { + // loadRulePack throws on validateRulePack failure, so a non-throwing non-empty load proves + // both presence and schema validity. + for (const category of ["dedup", "filler", "ultra"]) { + const rules = loadRulePack("zh", category, { refresh: true }); + assert.ok(rules.length > 0, `zh/${category} should have rules`); + } +}); + +test("zh: the pack shrinks a representative sample (classical terseness)", () => { + const all = loadAllRulesForLanguage("zh", { refresh: true }); + const sample = "请帮我修复数据库的错误,谢谢。如前所述,应用程序需要修复。"; + const out = applyAll(sample, all); + assert.ok( + out.length < sample.length, + `zh pack should shrink the sample (${sample.length} -> ${out.length})` + ); +}); + +test("zh: loadAllRulesForLanguage exposes dedup, filler and ultra categories", () => { + const cats = new Set(loadAllRulesForLanguage("zh", { refresh: true }).map((r) => r.category)); + assert.ok(cats.has("dedup"), "zh should expose dedup"); + assert.ok(cats.has("filler"), "zh should expose filler"); + assert.ok(cats.has("ultra"), "zh should expose ultra"); +}); + +test("detectCompressionLanguage: Han without kana → zh", () => { + assert.equal(detectCompressionLanguage("请帮我修复这个文件的错误"), "zh"); + assert.equal(detectCompressionLanguage("这个应用程序的数据库配置有问题"), "zh"); +}); + +test("detectCompressionLanguage: kana still wins for Japanese (no zh regression)", () => { + // Han-heavy Japanese with kana must stay ja, not flip to zh. + assert.equal(detectCompressionLanguage("このコードを修正してください"), "ja"); + assert.equal(detectCompressionLanguage("実装の認証を修正する必要があります"), "ja"); +}); + +test("detectCompressionLanguage: non-CJK languages are unaffected", () => { + assert.equal(detectCompressionLanguage("necesito corregir este archivo con error"), "es"); + assert.equal(detectCompressionLanguage("ich brauche diese konfiguration"), "de"); +}); + +test("zh is reported as a supported compression language", () => { + assert.ok(listSupportedCompressionLanguages().includes("zh")); +}); diff --git a/tests/unit/chatcore-nonstreaming-json-response.test.ts b/tests/unit/chatcore-nonstreaming-json-response.test.ts new file mode 100644 index 0000000000..c2edc82776 --- /dev/null +++ b/tests/unit/chatcore-nonstreaming-json-response.test.ts @@ -0,0 +1,22 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { buildNonStreamingJsonResponse } from "../../open-sse/handlers/chatCore/nonStreamingJsonResponse"; + +test("buildNonStreamingJsonResponse sets content-length for the serialized JSON body", async () => { + const response = buildNonStreamingJsonResponse( + { + id: "chatcmpl-test", + choices: [{ message: { role: "assistant", content: "hello" } }], + }, + { + "Content-Type": "application/json", + "X-OmniRoute-Cache": "MISS", + } + ); + + const text = await response.text(); + assert.equal(response.headers.get("Content-Type"), "application/json"); + assert.equal(response.headers.get("X-OmniRoute-Cache"), "MISS"); + assert.equal(response.headers.get("Content-Length"), String(Buffer.byteLength(text))); +}); diff --git a/tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts b/tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts new file mode 100644 index 0000000000..0c6ed5649f --- /dev/null +++ b/tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts @@ -0,0 +1,82 @@ +// #5531 — chatgpt-web sentinel PoW crashes on the Electron desktop app with +// "Digest method not supported" because Electron's BoringSSL lacks SHA-3 +// (electron/electron#30530). The PoW must hash through a runtime-portable +// SHA3-512 that falls back to a pure-JS Keccak when native SHA-3 is absent. + +import test from "node:test"; +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +const { sha3_512Hex, sha3_512HexJs, __setSha3NativeForTesting } = + await import("../../open-sse/utils/sha3-512.ts"); + +// FIPS-202 known-answer vectors for SHA3-512. +const FIPS: Record = { + "": + "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a6" + + "15b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26", + abc: + "b751850b1a57168a5693cd924b6b096e08f621827444f70d884f5d0240d2712e" + + "10e116e9192af3c91a7ec57647e3934057340b4cf408d5a56592f8274eec53f0", +}; + +function nativeSha3Available(): boolean { + try { + createHash("sha3-512").update(Buffer.alloc(0)).digest("hex"); + return true; + } catch { + return false; + } +} + +test("pure-JS SHA3-512 matches the FIPS-202 known-answer vectors", () => { + for (const [msg, want] of Object.entries(FIPS)) { + assert.equal(sha3_512HexJs(msg), want, `FIPS-202 vector mismatch for "${msg}"`); + } +}); + +test("pure-JS SHA3-512 is bit-identical to native createHash('sha3-512') on 300 random inputs", () => { + if (!nativeSha3Available()) { + // Runtime without native SHA-3 (e.g. an Electron CI) — FIPS vectors already cover correctness. + return; + } + for (let i = 0; i < 300; i++) { + const len = (i * 7) % 200; // spans multi-block (>72B) and exact-block-boundary cases + const buf = Buffer.alloc(len); + for (let j = 0; j < len; j++) buf[j] = (i * 31 + j * 17) & 0xff; + const native = createHash("sha3-512").update(buf).digest("hex"); + assert.equal(sha3_512HexJs(buf), native, `mismatch vs native at len=${len}`); + } +}); + +test("sha3_512Hex falls back to pure-JS when native SHA-3 is unavailable (Electron/BoringSSL sim) — #5531", () => { + __setSha3NativeForTesting(null); // simulate BoringSSL: createHash('sha3-512') would throw + try { + assert.equal(sha3_512Hex("abc"), FIPS.abc); + assert.equal(sha3_512Hex(""), FIPS[""]); + assert.equal(sha3_512Hex(Buffer.from("abc")), FIPS.abc); + } finally { + __setSha3NativeForTesting(undefined); // restore auto-detect + } +}); + +test("sha3_512Hex uses the native digest where available (parity with fallback)", () => { + if (!nativeSha3Available()) return; + __setSha3NativeForTesting(undefined); // force re-probe → native + assert.equal(sha3_512Hex("abc"), FIPS.abc); + assert.equal(sha3_512Hex("abc"), sha3_512HexJs("abc")); +}); + +test("chatgpt-web PoW routes SHA3-512 through the portable helper, not inline createHash (#5531 guard)", async () => { + const execPath = fileURLToPath( + new URL("../../open-sse/executors/chatgpt-web.ts", import.meta.url) + ); + const src = await readFile(execPath, "utf8"); + assert.ok( + !/createHash\(\s*["']sha3-512["']\s*\)/.test(src), + "PoW must NOT call native createHash('sha3-512') inline — it crashes under Electron/BoringSSL" + ); + assert.ok(/sha3_512Hex\s*\(/.test(src), "chatgpt-web PoW must hash via sha3_512Hex()"); +}); diff --git a/tests/unit/chatgpt-web.test.ts b/tests/unit/chatgpt-web.test.ts index 2dcd5d49e5..4191ff196e 100644 --- a/tests/unit/chatgpt-web.test.ts +++ b/tests/unit/chatgpt-web.test.ts @@ -1,20 +1,27 @@ import test from "node:test"; import assert from "node:assert/strict"; +import { writeFile } from "node:fs/promises"; const { ChatGptWebExecutor, __derivePublicBaseUrlForTesting, __resetChatGptWebCachesForTesting } = await import("../../open-sse/executors/chatgpt-web.ts"); const { describeChatGptWebHttpError } = await import("../../open-sse/executors/chatgptWebErrors.ts"); const { getExecutor, hasSpecializedExecutor } = await import("../../open-sse/executors/index.ts"); -const { __setTlsFetchOverrideForTesting, looksLikeSse, TlsClientUnavailableError } = - await import("../../open-sse/services/chatgptTlsClient.ts"); +const { + __setTlsFetchOverrideForTesting, + __tlsFetchStreamingForTesting, + looksLikeSse, + TlsClientUnavailableError, +} = await import("../../open-sse/services/chatgptTlsClient.ts"); // ─── Helpers ──────────────────────────────────────────────────────────────── function mockChatGptStreamText(events) { const chunks = []; for (const evt of events) { - chunks.push(`data: ${JSON.stringify(evt)}\r\n\r\n`); + const { __event, ...payload } = evt; + if (__event) chunks.push(`event: ${__event}\r\n`); + chunks.push(`data: ${JSON.stringify(payload)}\r\n\r\n`); } chunks.push("data: [DONE]\r\n\r\n"); return chunks.join(""); @@ -65,6 +72,7 @@ function installMockFetch({ dpl, fileDownload, attachmentDownload, + conversationDetail, signedDownload, userConfig, onSession, @@ -81,6 +89,7 @@ function installMockFetch({ conv: 0, fileDownload: 0, attachmentDownload: 0, + conversationDetail: 0, signedDownload: 0, userConfig: 0, userConfigUrls: [], @@ -235,6 +244,45 @@ function installMockFetch({ }; } + // /backend-api/conversation/ — detail poll used by GPT-5.5 Pro handoff. + { + const m1 = u.match(/\/backend-api\/conversation\/([^/?#]+)$/); + if (m1) { + calls.conversationDetail++; + const cfg = Array.isArray(conversationDetail) + ? (conversationDetail[ + Math.min(calls.conversationDetail - 1, conversationDetail.length - 1) + ] ?? conversationDetail[conversationDetail.length - 1]) + : (conversationDetail ?? { + status: 200, + body: { + mapping: { + "msg-final": { + message: { + id: "msg-final", + author: { role: "assistant" }, + content: { content_type: "text", parts: ["Final answer from poll."] }, + status: "finished_successfully", + end_turn: true, + create_time: 1, + update_time: 1, + }, + }, + }, + }, + }); + const text = typeof cfg.body === "string" ? cfg.body : JSON.stringify(cfg.body || {}); + return { + status: cfg.status, + headers: makeHeaders({ "Content-Type": "application/json" }), + text: opts.byteResponse + ? `data:application/json;base64,${Buffer.from(text, "utf8").toString("base64")}` + : text, + body: null, + }; + } + } + // Match only the exact conversation endpoint, not /conversations (plural — warmup). if ( u.endsWith("/backend-api/f/conversation") || @@ -743,6 +791,140 @@ test("Streaming: cumulative parts are diffed into non-overlapping deltas", async } }); +test("GPT-5.5 Pro non-streaming: stream_handoff polls conversation detail for final answer", async () => { + reset(); + const m = installMockFetch({ + conv: { + status: 200, + events: [ + { + conversation_id: "conv-pro", + message: { + id: "progress-1", + author: { role: "assistant" }, + content: { content_type: "text", parts: ["Working on it…"] }, + status: "in_progress", + }, + }, + { __event: "stream_handoff", conversation_id: "conv-pro" }, + ], + }, + conversationDetail: { + status: 200, + body: { + mapping: { + thought: { + message: { + id: "thought", + author: { role: "assistant" }, + content: { content_type: "thoughts", parts: ["hidden thinking"] }, + status: "finished_successfully", + end_turn: true, + create_time: 1, + update_time: 1, + }, + }, + final: { + message: { + id: "final", + author: { role: "assistant" }, + content: { content_type: "text", parts: ["👉 Final full Pro answer."] }, + status: "finished_successfully", + end_turn: true, + create_time: 2, + update_time: 2, + }, + }, + }, + }, + }, + }); + try { + const executor = new ChatGptWebExecutor(); + const result = await executor.execute({ + model: "gpt-5-5-pro-extended", + body: { messages: [{ role: "user", content: "hard problem" }] }, + stream: false, + credentials: { apiKey: "cookie-pro-poll" }, + signal: AbortSignal.timeout(10_000), + log: null, + }); + + const json = await result.response.json(); + assert.equal(json.choices[0].message.content, "👉 Final full Pro answer."); + assert.equal(m.calls.conversationDetail, 1); + const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation")); + const sentBody = JSON.parse(m.calls.bodies[convIdx]); + assert.equal(sentBody.history_and_training_disabled, true); + } finally { + m.restore(); + } +}); + +test("GPT-5.5 Pro streaming: preserves interim reasoning and appends final polled answer", async () => { + reset(); + const m = installMockFetch({ + conv: { + status: 200, + events: [ + { + conversation_id: "conv-pro-stream", + message: { + id: "progress-1", + author: { role: "assistant" }, + content: { + content_type: "text", + parts: ["Interim reasoning text"], + }, + status: "in_progress", + }, + }, + { __event: "stream_handoff", conversation_id: "conv-pro-stream" }, + ], + }, + conversationDetail: { + status: 200, + body: { + mapping: { + final: { + message: { + id: "final-stream", + author: { role: "assistant" }, + content: { content_type: "text", parts: ["👉 Final streamed Pro answer."] }, + status: "finished_successfully", + end_turn: true, + create_time: 1, + update_time: 1, + }, + }, + }, + }, + }, + }); + try { + const executor = new ChatGptWebExecutor(); + const result = await executor.execute({ + model: "gpt-5-5-pro-extended", + body: { messages: [{ role: "user", content: "hard problem" }], stream: true }, + stream: true, + credentials: { apiKey: "cookie-pro-stream" }, + signal: AbortSignal.timeout(10_000), + log: null, + }); + + const text = await result.response.text(); + assert.match(text, /Interim reasoning text<\/thinking>/); + assert.match(text, /👉 Final streamed Pro answer\./); + assert.ok( + text.indexOf("👉 Final streamed Pro answer.") > text.indexOf("Interim reasoning text"), + "final polled answer should be appended after interim reasoning" + ); + assert.equal(m.calls.conversationDetail, 1); + } finally { + m.restore(); + } +}); + // ─── Errors ───────────────────────────────────────────────────────────────── test("Error: 401 on /api/auth/session returns 401 with re-paste hint", async () => { @@ -1092,30 +1274,34 @@ test("Provider registry: chatgpt-web exposes the current ChatGPT Web model catal // Mirrors /backend-api/models for ChatGPT Web. Retired GPT-5/GPT-5.1 // entries should stay out of this list. assert.deepEqual(ids, [ - "gpt-5.5-pro", - "gpt-5.5-thinking", - "gpt-5.5", - "gpt-5.4-pro", - "gpt-5.4-thinking", - "gpt-5.4-thinking-mini", - "gpt-5.3", - "gpt-5.3-mini", + "gpt-5-5-pro", + "gpt-5-5-pro-extended", + "gpt-5-5-thinking", + "gpt-5-5", + "gpt-5-4-pro", + "gpt-5-4-thinking", + "gpt-5-4-t-mini", + "gpt-5-3", + "gpt-5-3-mini", "o3", ]); }); -test("Executor MODEL_MAP: dot-form OmniRoute IDs translate to dash-form ChatGPT slugs", async () => { +test("Executor MODEL_MAP: OmniRoute IDs translate to ChatGPT backend slugs", async () => { reset(); const m = installMockFetch(); try { const cases: Array<[string, string]> = [ + ["gpt-5-3", "gpt-5-3"], + ["gpt-5-5-thinking", "gpt-5-5-thinking"], + ["gpt-5-4-t-mini", "gpt-5-4-t-mini"], + ["gpt-5-5-pro", "gpt-5-5-pro"], + ["gpt-5-5-pro-extended", "gpt-5-5-pro"], + ["o3", "o3"], + // Legacy dot-form ids are still accepted for direct provider/model callers. ["gpt-5.3", "gpt-5-3"], ["gpt-5.5-thinking", "gpt-5-5-thinking"], ["gpt-5.4-thinking-mini", "gpt-5-4-t-mini"], - ["o3", "o3"], - // Regression #4665: these advertised catalog ids were missing from - // MODEL_MAP and fell through as their dot-form slug verbatim, which the - // ChatGPT backend-api silently rejects → served the default Plus model. ["gpt-5.5", "gpt-5-5"], ["gpt-5.5-pro", "gpt-5-5-pro"], ["gpt-5.4-pro", "gpt-5-4-pro"], @@ -1141,17 +1327,13 @@ test("Executor MODEL_MAP: dot-form OmniRoute IDs translate to dash-form ChatGPT } }); -test("MODEL_MAP drift guard: every advertised dot-form catalog id resolves to a dash-form backend slug (no verbatim fall-through) (#4665)", async () => { +test("MODEL_MAP drift guard: every advertised catalog id reaches ChatGPT as a backend slug", async () => { reset(); const { getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts"); const ids = (getRegistryEntry("chatgpt-web")?.models || []).map((m) => m.id); - // Dot-form ids must never reach the ChatGPT backend verbatim — the backend - // only accepts dash-form slugs. (Ids without a dot — e.g. "o3", or any - // already-slug-form id — pass through unchanged and are exempt.) - const dotFormIds = ids.filter((id) => id.includes(".")); const m = installMockFetch(); try { - for (const omniId of dotFormIds) { + for (const omniId of ids) { m.calls.urls.length = 0; m.calls.bodies.length = 0; const executor = new ChatGptWebExecutor(); @@ -1167,13 +1349,13 @@ test("MODEL_MAP drift guard: every advertised dot-form catalog id resolves to a const body = JSON.parse(m.calls.bodies[convIdx]); assert.ok( !body.model.includes("."), - `${omniId} reached the backend as "${body.model}" (still dot-form) — missing MODEL_MAP entry causes silent model substitution` - ); - assert.notEqual( - body.model, - omniId, - `${omniId} fell through MODEL_MAP verbatim — add a dash-form mapping` + `${omniId} reached the backend as "${body.model}" (still dot-form)` ); + if (omniId.endsWith("-extended")) { + assert.equal(body.model, "gpt-5-5-pro", `${omniId} should select the base Pro slug`); + } else { + assert.equal(body.model, omniId, `${omniId} should pass through as its backend slug`); + } } } finally { m.restore(); @@ -1182,6 +1364,93 @@ test("MODEL_MAP drift guard: every advertised dot-form catalog id resolves to a // ─── thinking_effort PATCH user_last_used_model_config ───────────────────── +test("GPT-5.5 Pro Extended sends base slug with extended effort and Temporary Chat", async () => { + reset(); + const m = installMockFetch(); + try { + const executor = new ChatGptWebExecutor(); + const result = await executor.execute({ + model: "gpt-5-5-pro-extended", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: false, + credentials: { apiKey: "cookie-pro-extended" }, + signal: AbortSignal.timeout(10_000), + log: null, + }); + assert.equal(result.response.status, 200); + const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation")); + const body = JSON.parse(m.calls.bodies[convIdx]); + assert.equal(body.model, "gpt-5-5-pro"); + assert.equal(body.thinking_effort, "extended"); + assert.equal(body.history_and_training_disabled, true); + assert.equal( + m.calls.userConfig, + 0, + "Pro effort is sent with the turn, not PATCHed as a thinking-model preference" + ); + } finally { + m.restore(); + } +}); + +test("GPT-5.5 Pro standard sends standard effort", async () => { + reset(); + const m = installMockFetch(); + try { + const executor = new ChatGptWebExecutor(); + await executor.execute({ + model: "gpt-5-5-pro", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: false, + credentials: { apiKey: "cookie-pro-standard" }, + signal: AbortSignal.timeout(10_000), + log: null, + }); + const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation")); + const body = JSON.parse(m.calls.bodies[convIdx]); + assert.equal(body.model, "gpt-5-5-pro"); + assert.equal(body.thinking_effort, "standard"); + assert.equal(body.history_and_training_disabled, true); + } finally { + m.restore(); + } +}); + +test("GPT-5.5 Pro store:false keeps Temporary Chat enabled for background utility calls", async () => { + reset(); + const m = installMockFetch(); + try { + const executor = new ChatGptWebExecutor(); + const result = await executor.execute({ + model: "gpt-5-5-pro-extended", + body: { + store: false, + messages: [ + { role: "system", content: "You are a session namer." }, + { role: "user", content: "Generate a short session name." }, + ], + }, + stream: false, + credentials: { apiKey: "cookie-pro-store-false" }, + signal: AbortSignal.timeout(10_000), + log: null, + }); + assert.equal(result.response.status, 200); + const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation")); + const body = JSON.parse(m.calls.bodies[convIdx]); + assert.equal(body.model, "gpt-5-5-pro"); + assert.equal(body.thinking_effort, "extended"); + assert.equal(body.history_and_training_disabled, true); + assert.equal( + m.calls.conversationDetail, + 0, + "no final-answer poll is needed when the stream did not hand off" + ); + } finally { + m.restore(); + } +}); + test("thinking_effort: high → PATCH user_last_used_model_config with extended", async () => { reset(); const m = installMockFetch(); @@ -1873,6 +2142,48 @@ test("looksLikeSse: rejects non-SSE bodies that previously passed as 200", () => assert.equal(looksLikeSse("error: rate limit"), false, "non-SSE field name"); }); +test("tls streaming: late first byte is read from streamOutputPath instead of empty body", async () => { + const fakeClient = { + async request(_url, opts) { + await new Promise((resolve) => setTimeout(resolve, 25)); + await writeFile( + String(opts.streamOutputPath), + mockChatGptStreamText([ + { + conversation_id: "conv-late", + message: { + id: "msg-late", + author: { role: "assistant" }, + content: { content_type: "text", parts: ["Late title answer"] }, + status: "finished_successfully", + }, + }, + ]), + "utf8" + ); + return { + status: 200, + headers: { "content-type": ["text/event-stream"] }, + body: "", + }; + }, + }; + + const result = await __tlsFetchStreamingForTesting( + fakeClient, + "https://chatgpt.com/backend-api/f/conversation", + { method: "POST" }, + "[DONE]", + null, + 1_000, + 5 + ); + + assert.equal(result.status, 200); + assert.equal(result.body, null); + assert.match(result.text ?? "", /Late title answer/); +}); + // ─── Image generation ────────────────────────────────────────────────────── /** Build a SSE event stream that mimics ChatGPT's image-generation reply. diff --git a/tests/unit/claude-identity-version-sync.test.ts b/tests/unit/claude-identity-version-sync.test.ts new file mode 100644 index 0000000000..52c6ddf810 --- /dev/null +++ b/tests/unit/claude-identity-version-sync.test.ts @@ -0,0 +1,56 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +// Claude-Code identity version is hand-bumped in lockstep across several modules +// (2.1.158 → .187 → .195 …). A silent partial bump makes one surface advertise a stale +// `claude-cli/` and can break Anthropic identity gating. This guard fails on drift. +// (quota-share-hardening Phase 2 — gaps v3.8.42.) +const claudeIdentity = await import("../../open-sse/executors/claudeIdentity.ts"); +const ccBridge = await import("../../open-sse/services/ccBridgeTransforms.ts"); +const claudeCompat = await import("../../open-sse/services/claudeCodeCompatible.ts"); +const anthropicHeaders = await import("../../open-sse/config/anthropicHeaders.ts"); +const glmProvider = await import("../../open-sse/config/glmProvider.ts"); + +const CANONICAL = claudeIdentity.CLAUDE_CODE_VERSION; + +// "claude-cli/2.1.195 (external, sdk-cli)" → "2.1.195". String ops only — never a RegExp over +// the value, per the project's anti-ReDoS contract. +function versionFromUserAgent(userAgent: string): string { + const afterSlash = userAgent.split("claude-cli/")[1] ?? ""; + return afterSlash.split(" ")[0]; +} + +test("canonical claude-cli version constant is a sane semver value", () => { + assert.match(CANONICAL, /^\d+\.\d+\.\d+$/); +}); + +test("all Claude-Code identity version constants are in lockstep", () => { + assert.equal( + ccBridge.DEFAULT_CLAUDE_CODE_VERSION, + CANONICAL, + "ccBridgeTransforms.DEFAULT_CLAUDE_CODE_VERSION drifted from claudeIdentity.CLAUDE_CODE_VERSION" + ); + assert.equal( + claudeCompat.CLAUDE_CODE_COMPATIBLE_VERSION, + CANONICAL, + "claudeCodeCompatible.CLAUDE_CODE_COMPATIBLE_VERSION drifted from the canonical version" + ); + assert.equal( + anthropicHeaders.CLAUDE_CLI_VERSION, + CANONICAL, + "anthropicHeaders.CLAUDE_CLI_VERSION drifted from the canonical version" + ); +}); + +test("all claude-cli User-Agent strings embed the canonical version", () => { + assert.equal( + versionFromUserAgent(claudeCompat.CLAUDE_CODE_COMPATIBLE_USER_AGENT), + CANONICAL, + "claudeCodeCompatible.CLAUDE_CODE_COMPATIBLE_USER_AGENT embeds a stale version" + ); + assert.equal( + versionFromUserAgent(glmProvider.GLM_CLAUDE_CODE_USER_AGENT), + CANONICAL, + "glmProvider.GLM_CLAUDE_CODE_USER_AGENT embeds a stale version" + ); +}); diff --git a/tests/unit/claude-thinking-tool-choice-guard.test.ts b/tests/unit/claude-thinking-tool-choice-guard.test.ts index 8ef086f937..9cfcb1fd36 100644 --- a/tests/unit/claude-thinking-tool-choice-guard.test.ts +++ b/tests/unit/claude-thinking-tool-choice-guard.test.ts @@ -52,6 +52,11 @@ async function captureUpstreamBody( stream: false, // OAuth token (sk-ant-oat…) with NO apiKey => wire-image path fires. credentials: { accessToken: "sk-ant-oat-test-thinkguard" }, + // #5480: the default adaptive-thinking injection is gated behind a real Claude Code + // client (`x-app: cli` / `claude-code` UA). A bare OAuth token (generic OpenAI-compat + // client) must opt in via x-omniroute-thinking and no longer gets force-injected, so + // these tests now identify as a Claude Code client to exercise the adaptive path. + clientHeaders: { "x-app": "cli" }, }); } finally { globalThis.fetch = originalFetch; diff --git a/tests/unit/cloudAgent-types.test.ts b/tests/unit/cloudAgent-types.test.ts index dbff0ec2a4..b57436b07f 100644 --- a/tests/unit/cloudAgent-types.test.ts +++ b/tests/unit/cloudAgent-types.test.ts @@ -1,10 +1,9 @@ import test from "node:test"; import assert from "node:assert/strict"; -const { CLOUD_AGENT_STATUS, CloudAgentStatusSchema } = - await import("../../src/lib/cloudAgent/types.ts"); -const { CreateCloudAgentTaskSchema, UpdateCloudAgentTaskSchema } = - await import("../../src/lib/cloudAgent/types.ts"); +const cloudAgentTypes = await import("../../src/lib/cloudAgent/types.ts"); +const { CLOUD_AGENT_STATUS, CloudAgentStatusSchema } = cloudAgentTypes; +const { CreateCloudAgentTaskSchema, UpdateCloudAgentTaskSchema } = cloudAgentTypes; const { isCloudAgentProvider, getAgent, getAvailableAgents } = await import("../../src/lib/cloudAgent/registry.ts"); @@ -17,6 +16,21 @@ test("CLOUD_AGENT_STATUS has correct values", () => { assert.strictEqual(CLOUD_AGENT_STATUS.CANCELLED, "cancelled"); }); +test("cloud agent types module exposes the request validation schemas", () => { + const expectedRuntimeExports = [ + "CLOUD_AGENT_STATUS", + "CloudAgentSourceSchema", + "CloudAgentStatusSchema", + "CloudAgentTaskOptionsSchema", + "CreateCloudAgentTaskSchema", + "UpdateCloudAgentTaskSchema", + ]; + + for (const key of expectedRuntimeExports) { + assert.equal(key in cloudAgentTypes, true, `${key} should stay exported`); + } +}); + test("CloudAgentStatusSchema validates valid statuses", () => { const validStatuses = [ "queued", diff --git a/tests/unit/codexAuthZipExtract.test.ts b/tests/unit/codexAuthZipExtract.test.ts index 261e3de056..1d3fbae1c5 100644 --- a/tests/unit/codexAuthZipExtract.test.ts +++ b/tests/unit/codexAuthZipExtract.test.ts @@ -1,25 +1,9 @@ import test from "node:test"; import assert from "node:assert/strict"; import { zipSync, strToU8 } from "fflate"; - -// Mirror the safety logic from codexAuthZipExtract.ts so we can test without -// importing the module (which is fine since it has no external DB deps, but -// we test the pure logic to keep the test surface clear). - -interface ExtractedZipFile { - name: string; - content: string; -} - -interface ExtractZipOptions { - maxFiles?: number; - maxFileSizeBytes?: number; - maxTotalSizeBytes?: number; -} - -// Local re-implementation of the exported function to exercise it without -// importing Node-only code in the test runner. import { extractCodexAuthZip } from "../../src/lib/oauth/utils/codexAuthZipExtract.ts"; +import { extractClaudeAuthZip } from "../../src/lib/oauth/utils/claudeAuthZipExtract.ts"; +import { extractJsonZip } from "../../src/lib/oauth/utils/jsonZipExtract.ts"; // ──── Helpers ───────────────────────────────────────────────────────────────── @@ -47,6 +31,17 @@ test("extractCodexAuthZip: happy path — returns all .json entries", () => { assert.deepEqual(names, ["auth-a.json", "auth-b.json", "auth-c.json"]); }); +test("auth ZIP extractors share the generic JSON ZIP behavior", () => { + const zip = makeZip({ + "auth-a.json": VALID_AUTH, + "nested/auth-b.json": VALID_AUTH, + "README.md": "# Readme", + }); + + assert.deepEqual(extractCodexAuthZip(zip), extractJsonZip(zip)); + assert.deepEqual(extractClaudeAuthZip(zip), extractJsonZip(zip)); +}); + test("extractCodexAuthZip: ignores non-.json entries", () => { const zip = makeZip({ "auth-a.json": VALID_AUTH, diff --git a/tests/unit/combo-builder-model-source-5477.test.ts b/tests/unit/combo-builder-model-source-5477.test.ts new file mode 100644 index 0000000000..230e4fe48d --- /dev/null +++ b/tests/unit/combo-builder-model-source-5477.test.ts @@ -0,0 +1,45 @@ +/** + * #5477 — the combo builder's per-model option construction was extracted into a + * shared `buildModelOptions` helper (one source of truth for the synced / + * built-in / custom / fallback branches). This test drives the real builder and + * locks the custom-model **source classification** branch: a custom model whose + * stored `source` is one of api-sync/auto-sync/imported must surface as + * `imported`, everything else as `custom`. A silent divergence in that mapping is + * exactly what the extraction could introduce. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-source-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const modelsDb = await import("../../src/lib/db/models.ts"); +const { getComboBuilderOptions } = await import("../../src/lib/combos/builderOptions.ts"); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("#5477 buildModelOptions classifies custom-model source (manual -> custom, api-sync -> imported)", async () => { + // Attach to a no-auth provider ("opencode") — it surfaces in the builder + // without a configured connection, so the custom-model branch is exercised. + await modelsDb.addCustomModel("opencode", "zzz-manual-5477", "Manual 5477", "manual"); + await modelsDb.addCustomModel("opencode", "zzz-apisync-5477", "ApiSync 5477", "api-sync"); + + const payload = await getComboBuilderOptions(); + + const allModels = payload.providers.flatMap((p) => p.models); + const manual = allModels.find((m) => m.id === "zzz-manual-5477"); + const apiSync = allModels.find((m) => m.id === "zzz-apisync-5477"); + + assert.ok(manual, "manual custom model must appear in the combo builder output"); + assert.ok(apiSync, "api-sync custom model must appear in the combo builder output"); + + assert.equal(manual.source, "custom", "source=manual must classify as 'custom'"); + assert.equal(apiSync.source, "imported", "source=api-sync must classify as 'imported'"); +}); diff --git a/tests/unit/compression-pipeline-inflation-guard.test.ts b/tests/unit/compression-pipeline-inflation-guard.test.ts new file mode 100644 index 0000000000..07150a13d9 --- /dev/null +++ b/tests/unit/compression-pipeline-inflation-guard.test.ts @@ -0,0 +1,125 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { guardPipelineInflation } from "../../open-sse/services/compression/pipelineGuards.ts"; +import { applyStackedCompression } from "../../open-sse/services/compression/strategySelector.ts"; +import { + registerCompressionEngine, + setEngineEnabled, +} from "../../open-sse/services/compression/engines/registry.ts"; +import type { CompressionEngine } from "../../open-sse/services/compression/engines/types.ts"; + +// --- Pure guard logic --- + +test("guardPipelineInflation reverts to the original when the stacked output did not shrink", () => { + const original = { a: 1 }; + const compressed = { a: 1, pad: "xxxxxxxx" }; + const r = guardPipelineInflation({ + originalBody: original, + compressedBody: compressed, + originalTokens: 100, + compressedTokens: 120, + }); + assert.equal(r.inflated, true); + assert.equal(r.body, original); +}); + +test("guardPipelineInflation reverts on a net-zero (equal-token) result", () => { + const original = { a: 1 }; + const compressed = { b: 2 }; + const r = guardPipelineInflation({ + originalBody: original, + compressedBody: compressed, + originalTokens: 50, + compressedTokens: 50, + }); + assert.equal(r.inflated, true); + assert.equal(r.body, original); +}); + +test("guardPipelineInflation keeps the compressed body when it actually shrank", () => { + const original = { a: 1, pad: "xxxxxxxx" }; + const compressed = { a: 1 }; + const r = guardPipelineInflation({ + originalBody: original, + compressedBody: compressed, + originalTokens: 100, + compressedTokens: 40, + }); + assert.equal(r.inflated, false); + assert.equal(r.body, compressed); +}); + +test("guardPipelineInflation treats empty input as not inflated", () => { + const r = guardPipelineInflation({ + originalBody: {}, + compressedBody: {}, + originalTokens: 0, + compressedTokens: 0, + }); + assert.equal(r.inflated, false); +}); + +// --- Wire: an inflating engine in the stacked pipeline is reverted --- + +const INFLATE_ID = "test-inflate-guard"; + +const inflatingEngine: CompressionEngine = { + id: INFLATE_ID, + name: "Test Inflate", + description: "test-only engine that inflates the body", + icon: "bug_report", + targets: ["messages"], + stackable: true, + stackPriority: 0, + metadata: { + id: INFLATE_ID, + name: "Test Inflate", + description: "test-only", + inputScope: "messages", + targetLatencyMs: 0, + supportsPreview: false, + stable: true, + }, + apply(body) { + const inflated = { ...body, __pad: "x".repeat(4000) }; + return { + body: inflated, + compressed: true, + stats: { + originalTokens: 10, + compressedTokens: 1010, + savingsPercent: -10000, + techniquesUsed: [INFLATE_ID], + mode: "stacked", + timestamp: 0, + }, + }; + }, + compress(body) { + return this.apply(body); + }, + getConfigSchema() { + return []; + }, + validateConfig() { + return { valid: true, errors: [] }; + }, +}; + +test("applyStackedCompression reverts to the original body when the pipeline inflates", () => { + registerCompressionEngine(inflatingEngine); + setEngineEnabled(INFLATE_ID, true); + + const body = { + messages: [{ role: "user", content: "hello world this is a short message" }], + }; + const result = applyStackedCompression(body, [INFLATE_ID]); + + // The inflating engine produced a bigger body, so the aggregate guard discarded it. + assert.equal(result.compressed, false); + assert.deepEqual(result.body, body); + assert.ok( + (result.stats.validationWarnings ?? []).some((w) => w.includes("pipeline-inflation-guard")), + "expected the inflation-guard warning in stats.validationWarnings" + ); +}); diff --git a/tests/unit/compression/bailout.test.ts b/tests/unit/compression/bailout.test.ts index 46b97eca4e..ed46072f66 100644 --- a/tests/unit/compression/bailout.test.ts +++ b/tests/unit/compression/bailout.test.ts @@ -71,8 +71,9 @@ function makeLowGainEngine(id = LOW_GAIN_ENGINE_ID): CompressionEngine { ...makeBaseEngine(id), apply: (body) => { const messages = (body.messages as Array<{ role: string; content: string }>) ?? []; + // Tag the user message; drop any padding (non-user) content so the body shrinks. const next = messages.map((m) => - m.role === "user" ? { ...m, content: m.content + "|low" } : m + m.role === "user" ? { ...m, content: m.content + "|low" } : { ...m, content: "" } ); return { body: { ...body, messages: next }, @@ -97,8 +98,9 @@ const highGainEngine: CompressionEngine = { ...makeBaseEngine(HIGH_GAIN_ENGINE_ID), apply: (body) => { const messages = (body.messages as Array<{ role: string; content: string }>) ?? []; + // Tag the user message; drop any padding (non-user) content so the body shrinks. const next = messages.map((m) => - m.role === "user" ? { ...m, content: m.content + "|high" } : m + m.role === "user" ? { ...m, content: m.content + "|high" } : { ...m, content: "" } ); return { body: { ...body, messages: next }, @@ -146,6 +148,22 @@ function userContent(result: CompressionResult): string { const BAILOUT_ON = { bailout: { enabled: true, minGainPercent: 10 } }; const BAILOUT_OFF = {}; // default — no bailout field +// A fixture body carrying a large droppable padding message. When an engine runs it +// drops that padding (and tags the user message), so the FINAL stacked body genuinely +// shrinks — keeping the #5527 (T02) inflation guard from reverting these advancement +// fixtures. The guard only reverts a pipeline whose final body did NOT shrink in tokens; +// these tests exercise bail-out/advancement, an orthogonal concern, so the body must +// realistically shrink for the marker assertions to survive the guard. +const PADDING = "padding tokens ".repeat(40); +function mkBody(): { messages: Array<{ role: string; content: string }> } { + return { + messages: [ + { role: "user", content: "hello" }, + { role: "assistant", content: PADDING }, + ], + }; +} + // ── suite ──────────────────────────────────────────────────────────────────── describe("TV1 — stacked pipeline bail-out discipline (OPT-IN)", () => { @@ -169,7 +187,7 @@ describe("TV1 — stacked pipeline bail-out discipline (OPT-IN)", () => { describe("sync — applyStackedCompression", () => { it("bail-out ON: throwing engine → step skipped, pipeline does NOT throw", () => { - const body = { messages: [{ role: "user", content: "hello" }] }; + const body = mkBody(); // Must not throw and original body is kept (throw engine was the only step) const result = applyStackedCompression(body, pipeline(THROW_ENGINE_ID), BAILOUT_ON); @@ -186,7 +204,7 @@ describe("TV1 — stacked pipeline bail-out discipline (OPT-IN)", () => { }); it("bail-out ON: throwing engine before a good engine → good engine still runs", () => { - const body = { messages: [{ role: "user", content: "hello" }] }; + const body = mkBody(); // throw engine first, then high-gain engine — the high-gain must still run const result = applyStackedCompression( @@ -201,7 +219,7 @@ describe("TV1 — stacked pipeline bail-out discipline (OPT-IN)", () => { }); it("bail-out ON: low-gain engine (5%) → body NOT advanced (step skipped)", () => { - const body = { messages: [{ role: "user", content: "hello" }] }; + const body = mkBody(); const result = applyStackedCompression(body, pipeline(LOW_GAIN_ENGINE_ID), BAILOUT_ON); @@ -212,7 +230,7 @@ describe("TV1 — stacked pipeline bail-out discipline (OPT-IN)", () => { }); it("bail-out ON: high-gain engine (20%) → body IS advanced normally", () => { - const body = { messages: [{ role: "user", content: "hello" }] }; + const body = mkBody(); const result = applyStackedCompression(body, pipeline(HIGH_GAIN_ENGINE_ID), BAILOUT_ON); @@ -221,7 +239,7 @@ describe("TV1 — stacked pipeline bail-out discipline (OPT-IN)", () => { }); it("bail-out ON: low-gain then high-gain → only high-gain advances body", () => { - const body = { messages: [{ role: "user", content: "hello" }] }; + const body = mkBody(); const result = applyStackedCompression( body, @@ -234,7 +252,7 @@ describe("TV1 — stacked pipeline bail-out discipline (OPT-IN)", () => { }); it("bail-out OFF (default): low-gain engine IS applied (opt-in guard)", () => { - const body = { messages: [{ role: "user", content: "hello" }] }; + const body = mkBody(); // No bailout config at all — original behavior const result = applyStackedCompression(body, pipeline(LOW_GAIN_ENGINE_ID), BAILOUT_OFF); @@ -245,7 +263,7 @@ describe("TV1 — stacked pipeline bail-out discipline (OPT-IN)", () => { }); it("bail-out OFF (default): throwing engine propagates — unchanged existing behavior", () => { - const body = { messages: [{ role: "user", content: "hello" }] }; + const body = mkBody(); // Without bail-out, a throw is NOT caught → pipeline throws assert.throws(() => { @@ -258,7 +276,7 @@ describe("TV1 — stacked pipeline bail-out discipline (OPT-IN)", () => { describe("async — applyStackedCompressionAsync", () => { it("bail-out ON: async throwing engine → step skipped, no throw", async () => { - const body = { messages: [{ role: "user", content: "hello" }] }; + const body = mkBody(); const result = await applyStackedCompressionAsync(body, pipeline(THROW_ASYNC_ID), BAILOUT_ON); @@ -267,7 +285,7 @@ describe("TV1 — stacked pipeline bail-out discipline (OPT-IN)", () => { }); it("bail-out ON: async low-gain engine (5%) → step skipped", async () => { - const body = { messages: [{ role: "user", content: "hello" }] }; + const body = mkBody(); const result = await applyStackedCompressionAsync( body, @@ -280,7 +298,7 @@ describe("TV1 — stacked pipeline bail-out discipline (OPT-IN)", () => { }); it("bail-out OFF (default): async low-gain engine IS applied", async () => { - const body = { messages: [{ role: "user", content: "hello" }] }; + const body = mkBody(); const result = await applyStackedCompressionAsync( body, diff --git a/tests/unit/compression/engine-enabled-toggle.test.ts b/tests/unit/compression/engine-enabled-toggle.test.ts index 958d419e6d..544bb6b753 100644 --- a/tests/unit/compression/engine-enabled-toggle.test.ts +++ b/tests/unit/compression/engine-enabled-toggle.test.ts @@ -53,8 +53,10 @@ function makeTaggingEngine(id: string): CompressionEngine { validateConfig: () => ({ valid: true, errors: [] }), apply: (body) => { const messages = (body.messages as Array<{ role: string; content: string }>) ?? []; + // Tag the user message; drop any padding (non-user) content so the body shrinks + // and the #5527 (T02) inflation guard keeps the tagged output instead of reverting. const next = messages.map((m) => - m.role === "user" ? { ...m, content: m.content + "|tagged" } : m + m.role === "user" ? { ...m, content: m.content + "|tagged" } : { ...m, content: "" } ); return { body: { ...body, messages: next }, @@ -83,7 +85,14 @@ function userContent(result: CompressionResult): string { } function freshBody() { - return { messages: [{ role: "user", content: "hi" }] }; + // Includes a droppable padding message so an engine that runs nets a real token shrink + // (the engine empties non-user content), keeping the #5527 inflation guard from reverting. + return { + messages: [ + { role: "user", content: "hi" }, + { role: "assistant", content: "padding tokens ".repeat(40) }, + ], + }; } describe("registry enabled toggle — stacked loop honors setEngineEnabled", () => { diff --git a/tests/unit/compression/stacked-async.test.ts b/tests/unit/compression/stacked-async.test.ts index 81a864b046..9fb8743467 100644 --- a/tests/unit/compression/stacked-async.test.ts +++ b/tests/unit/compression/stacked-async.test.ts @@ -27,8 +27,10 @@ const FA = "fake-async-engine"; */ function tag(id: string, body: Record): CompressionResult { const messages = (body.messages as Array<{ role: string; content: string }>) ?? []; + // Tag the user message; drop any padding (non-user) content so the stacked body + // genuinely shrinks and the #5527 (T02) inflation guard does not revert it. const next = messages.map((m) => - m.role === "user" ? { ...m, content: `${m.content}|${id}` } : m + m.role === "user" ? { ...m, content: `${m.content}|${id}` } : { ...m, content: "" } ); return { body: { ...body, messages: next }, @@ -87,6 +89,20 @@ function pipeline(...ids: string[]): CompressionPipelineStep[] { return ids.map((engine) => ({ engine })) as unknown as CompressionPipelineStep[]; } +// Fixture body with a large droppable padding message: engines drop it (see `tag`) so the +// final stacked body shrinks and the #5527 (T02) inflation guard keeps the tagged output +// instead of reverting it. These tests assert engine ORDER/advancement, not compression +// ratios, so the body must realistically shrink for the tag assertions to survive the guard. +const PADDING = "padding tokens ".repeat(40); +function mkBody(): { messages: Array<{ role: string; content: string }> } { + return { + messages: [ + { role: "user", content: "hello" }, + { role: "assistant", content: PADDING }, + ], + }; +} + describe("stacked compression — async interface (H10)", () => { before(() => { registerCompressionEngine(makeEngine(FS)); @@ -98,7 +114,7 @@ describe("stacked compression — async interface (H10)", () => { }); it("runs a mixed sync+async pipeline in pipeline order", async () => { - const body = { messages: [{ role: "user", content: "hello" }] }; + const body = mkBody(); const result = await applyStackedCompressionAsync(body, pipeline(FS, FA)); assert.equal(userContent(result), "hello|fake-sync-engine|fake-async-engine"); @@ -111,7 +127,7 @@ describe("stacked compression — async interface (H10)", () => { }); it("preserves order when the async engine runs first", async () => { - const body = { messages: [{ role: "user", content: "hello" }] }; + const body = mkBody(); const result = await applyStackedCompressionAsync(body, pipeline(FA, FS)); assert.equal(userContent(result), "hello|fake-async-engine|fake-sync-engine"); @@ -122,7 +138,7 @@ describe("stacked compression — async interface (H10)", () => { }); it("async path yields the same result as sync path for sync-only engines", async () => { - const body = { messages: [{ role: "user", content: "hello" }] }; + const body = mkBody(); const asyncResult = await applyStackedCompressionAsync(body, pipeline(FS)); const syncResult = applyStackedCompression(body, pipeline(FS)); @@ -131,7 +147,7 @@ describe("stacked compression — async interface (H10)", () => { }); it("legacy sync path gracefully skips async-only work without crashing", () => { - const body = { messages: [{ role: "user", content: "hello" }] }; + const body = mkBody(); const result = applyStackedCompression(body, pipeline(FA)); // The async-only engine's sync apply() is a pass-through: no transform. diff --git a/tests/unit/context-editing-executor-injection.test.ts b/tests/unit/context-editing-executor-injection.test.ts index 44864f1c8e..799d9e85a6 100644 --- a/tests/unit/context-editing-executor-injection.test.ts +++ b/tests/unit/context-editing-executor-injection.test.ts @@ -35,6 +35,60 @@ function toolUseEdits(body: Record | undefined) { return (cm?.edits ?? []).filter((e) => e?.type === CLEAR_TOOL_USES_STRATEGY); } +test("#5312: generic Claude OAuth clients do not get implicit adaptive thinking", async () => { + const { bodies, restore } = mockFetchCapture(); + try { + await new DefaultExecutor("claude").execute({ + model: "claude-opus-4-8", + body: { + model: "claude-opus-4-8", + messages: [{ role: "user", content: "hi" }], + max_tokens: 1, + }, + stream: false, + credentials: { accessToken: "sk-ant-oat-generic-client-token" }, + clientHeaders: { + "user-agent": "Cursor/1.0", + }, + contextEditing: { enabled: false }, + }); + } finally { + restore(); + } + + assert.equal(bodies[0]?.thinking, undefined); + assert.equal(bodies[0]?.output_config, undefined); + assert.equal(bodies[0]?.context_management, undefined); +}); + +test("#5312: generic Claude OAuth clients can opt in to adaptive thinking", async () => { + const { bodies, restore } = mockFetchCapture(); + try { + await new DefaultExecutor("claude").execute({ + model: "claude-opus-4-8", + body: { + model: "claude-opus-4-8", + messages: [{ role: "user", content: "hi" }], + max_tokens: 1, + }, + stream: false, + credentials: { accessToken: "sk-ant-oat-generic-client-token" }, + clientHeaders: { + "user-agent": "Cursor/1.0", + "x-omniroute-thinking": "adaptive", + }, + contextEditing: { enabled: false }, + }); + } finally { + restore(); + } + + assert.deepEqual(bodies[0]?.thinking, { type: "adaptive" }); + assert.deepEqual(bodies[0]?.context_management, { + edits: [{ type: CLEAR_THINKING_STRATEGY, keep: "all" }], + }); +}); + test("Context Editing: enabled → genuine claude request gets clear_tool_uses with defaults", async () => { const { bodies, restore } = mockFetchCapture(); try { diff --git a/tests/unit/copilot-codegraph-knowledge.test.ts b/tests/unit/copilot-codegraph-knowledge.test.ts new file mode 100644 index 0000000000..fce2886f59 --- /dev/null +++ b/tests/unit/copilot-codegraph-knowledge.test.ts @@ -0,0 +1,34 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + findCallers, + getCodeGraphStats, + isCodeGraphAvailable, + searchSymbols, +} from "../../src/lib/copilot/codegraphKnowledge.ts"; + +test("CodeGraph knowledge helpers fail closed when the index is unavailable", () => { + assert.equal(isCodeGraphAvailable(), false); + + assert.deepEqual(searchSymbols("handleChatCore"), { + success: false, + data: null, + error: "CodeGraph DB not found", + engine: "none", + }); + + assert.deepEqual(findCallers("handleChatCore"), { + success: false, + data: null, + error: "CodeGraph DB not found", + engine: "none", + }); + + assert.deepEqual(getCodeGraphStats(), { + success: false, + data: null, + error: "CodeGraph DB not found", + engine: "none", + }); +}); diff --git a/tests/unit/coze-validation-error-5426.test.ts b/tests/unit/coze-validation-error-5426.test.ts new file mode 100644 index 0000000000..8a2af32445 --- /dev/null +++ b/tests/unit/coze-validation-error-5426.test.ts @@ -0,0 +1,56 @@ +// #5426 — Coze key validation must surface a friendly message instead of leaking +// the raw upstream error envelope ({ code, msg, logId, from }) into the UI. +import { strict as assert } from "node:assert"; +import { describe, it } from "node:test"; +import { extractCozeValidationError } from "@/lib/providers/validation/cozeError"; + +describe("extractCozeValidationError (#5426)", () => { + it("builds a friendly message from a Coze envelope with msg + code", () => { + const body = { + code: 4100, + msg: "The token you entered is incorrect. Please check and try again.", + logId: "20240101000000ABCDEF", + from: "bot-api", + }; + const result = extractCozeValidationError(body); + assert.equal( + result, + "Coze rejected the key: The token you entered is incorrect. Please check and try again. (code 4100)" + ); + // Never echo the raw logId or the whole envelope. + assert.ok(result && !result.includes("20240101000000ABCDEF")); + assert.ok(result && !result.includes("logId")); + }); + + it('recognizes the from:"bot-api" variant', () => { + const body = { code: 700012006, msg: "rejected", from: "bot-api" }; + const result = extractCozeValidationError(body); + assert.equal(result, "Coze rejected the key: rejected (code 700012006)"); + }); + + it("recognizes a stringified JSON envelope", () => { + const body = JSON.stringify({ msg: "bad key", code: 4100 }); + assert.equal(extractCozeValidationError(body), "Coze rejected the key: bad key (code 4100)"); + }); + + it("returns null for a normal OpenAI error envelope", () => { + const body = { + error: { + message: "Incorrect API key provided", + type: "invalid_request_error", + code: "invalid_api_key", + }, + }; + assert.equal(extractCozeValidationError(body), null); + }); + + it("returns null for non-object / empty / non-JSON inputs", () => { + assert.equal(extractCozeValidationError(null), null); + assert.equal(extractCozeValidationError(undefined), null); + assert.equal(extractCozeValidationError(42), null); + assert.equal(extractCozeValidationError(""), null); + assert.equal(extractCozeValidationError("not json at all"), null); + assert.equal(extractCozeValidationError({}), null); + assert.equal(extractCozeValidationError([]), null); + }); +}); diff --git a/tests/unit/dashboard/providerPageStorage.test.ts b/tests/unit/dashboard/providerPageStorage.test.ts new file mode 100644 index 0000000000..94a6fe24ad --- /dev/null +++ b/tests/unit/dashboard/providerPageStorage.test.ts @@ -0,0 +1,229 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + parseConfiguredOnlyPreference, + parseProviderDisplayModePreference, + readConfiguredOnlyPreference, + writeConfiguredOnlyPreference, + readProviderDisplayModePreference, + shouldSyncProviderDisplayMode, + writeProviderDisplayModePreference, + SHOW_CONFIGURED_ONLY_STORAGE_KEY, + PROVIDER_DISPLAY_MODE_STORAGE_KEY, +} from "../../../src/app/(dashboard)/dashboard/providers/providerPageStorage"; + +// --------------------------------------------------------------------------- +// Helpers: in-memory storage mock +// --------------------------------------------------------------------------- +function makeMockStorage(): { + store: Map; + getItem(key: string): string | null; + setItem(key: string, value: string): void; + removeItem(key: string): void; +} { + const store = new Map(); + return { + store, + getItem(key: string) { + return store.get(key) ?? null; + }, + setItem(key: string, value: string) { + store.set(key, value); + }, + removeItem(key: string) { + store.delete(key); + }, + }; +} + +// --------------------------------------------------------------------------- +// parseConfiguredOnlyPreference +// --------------------------------------------------------------------------- +test("parseConfiguredOnlyPreference returns true for 'true'", () => { + assert.equal(parseConfiguredOnlyPreference("true"), true); +}); + +test("parseConfiguredOnlyPreference returns false for 'false'", () => { + assert.equal(parseConfiguredOnlyPreference("false"), false); +}); + +test("parseConfiguredOnlyPreference returns false for null", () => { + assert.equal(parseConfiguredOnlyPreference(null), false); +}); + +test("parseConfiguredOnlyPreference returns false for undefined", () => { + assert.equal(parseConfiguredOnlyPreference(undefined), false); +}); + +test("parseConfiguredOnlyPreference returns false for empty string", () => { + assert.equal(parseConfiguredOnlyPreference(""), false); +}); + +// --------------------------------------------------------------------------- +// parseProviderDisplayModePreference +// --------------------------------------------------------------------------- +test("parseProviderDisplayModePreference returns 'all' for 'all'", () => { + assert.equal(parseProviderDisplayModePreference("all"), "all"); +}); + +test("parseProviderDisplayModePreference returns 'configured' for 'configured'", () => { + assert.equal(parseProviderDisplayModePreference("configured"), "configured"); +}); + +test("parseProviderDisplayModePreference returns 'compact' for 'compact'", () => { + assert.equal(parseProviderDisplayModePreference("compact"), "compact"); +}); + +test("parseProviderDisplayModePreference returns null for invalid value", () => { + assert.equal(parseProviderDisplayModePreference("unknown"), null); +}); + +test("parseProviderDisplayModePreference returns null for null", () => { + assert.equal(parseProviderDisplayModePreference(null), null); +}); + +test("parseProviderDisplayModePreference returns null for undefined", () => { + assert.equal(parseProviderDisplayModePreference(undefined), null); +}); + +// --------------------------------------------------------------------------- +// readConfiguredOnlyPreference +// --------------------------------------------------------------------------- +test("readConfiguredOnlyPreference returns false when storage is null", () => { + assert.equal(readConfiguredOnlyPreference(null), false); +}); + +test("readConfiguredOnlyPreference reads value from storage", () => { + const storage = makeMockStorage(); + storage.setItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY, "true"); + assert.equal(readConfiguredOnlyPreference(storage), true); +}); + +test("readConfiguredOnlyPreference returns false when key is missing", () => { + const storage = makeMockStorage(); + assert.equal(readConfiguredOnlyPreference(storage), false); +}); + +// --------------------------------------------------------------------------- +// writeConfiguredOnlyPreference +// --------------------------------------------------------------------------- +test("writeConfiguredOnlyPreference does nothing when storage is null", () => { + // Should not throw + writeConfiguredOnlyPreference(true, null); +}); + +test("writeConfiguredOnlyPreference sets key to 'true' when enabled", () => { + const storage = makeMockStorage(); + writeConfiguredOnlyPreference(true, storage); + assert.equal(storage.getItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY), "true"); +}); + +test("writeConfiguredOnlyPreference removes key when disabled", () => { + const storage = makeMockStorage(); + storage.setItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY, "true"); + writeConfiguredOnlyPreference(false, storage); + assert.equal(storage.getItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY), null); +}); + +// --------------------------------------------------------------------------- +// readProviderDisplayModePreference (the main function used by the page) +// --------------------------------------------------------------------------- +test("readProviderDisplayModePreference returns 'all' when storage is null", () => { + assert.equal(readProviderDisplayModePreference(null), "all"); +}); + +test("readProviderDisplayModePreference reads stored display mode", () => { + const storage = makeMockStorage(); + storage.setItem(PROVIDER_DISPLAY_MODE_STORAGE_KEY, "configured"); + assert.equal(readProviderDisplayModePreference(storage), "configured"); +}); + +test("readProviderDisplayModePreference reads 'compact' mode", () => { + const storage = makeMockStorage(); + storage.setItem(PROVIDER_DISPLAY_MODE_STORAGE_KEY, "compact"); + assert.equal(readProviderDisplayModePreference(storage), "compact"); +}); + +test("readProviderDisplayModePreference returns 'all' when no preference stored", () => { + const storage = makeMockStorage(); + assert.equal(readProviderDisplayModePreference(storage), "all"); +}); + +test("readProviderDisplayModePreference migrates from old configured-only key", () => { + const storage = makeMockStorage(); + // Old key set but new key missing + storage.setItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY, "true"); + assert.equal(storage.getItem(PROVIDER_DISPLAY_MODE_STORAGE_KEY), null); + + // Should migrate: return "configured" AND write the new key + const result = readProviderDisplayModePreference(storage); + assert.equal(result, "configured"); + assert.equal(storage.getItem(PROVIDER_DISPLAY_MODE_STORAGE_KEY), "configured"); + assert.equal(storage.getItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY), null); +}); + +test("readProviderDisplayModePreference prefers new display mode key over old key", () => { + const storage = makeMockStorage(); + storage.setItem(PROVIDER_DISPLAY_MODE_STORAGE_KEY, "compact"); + storage.setItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY, "true"); + + // New key takes precedence — no migration + assert.equal(readProviderDisplayModePreference(storage), "compact"); + assert.equal(storage.getItem(PROVIDER_DISPLAY_MODE_STORAGE_KEY), "compact"); + assert.equal(storage.getItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY), "true"); +}); + +// --------------------------------------------------------------------------- +// writeProviderDisplayModePreference +// --------------------------------------------------------------------------- +test("writeProviderDisplayModePreference does nothing when storage is null", () => { + writeProviderDisplayModePreference("configured", null); +}); + +test("writeProviderDisplayModePreference stores 'configured' and cleans old key", () => { + const storage = makeMockStorage(); + storage.setItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY, "true"); + + writeProviderDisplayModePreference("configured", storage); + assert.equal(storage.getItem(PROVIDER_DISPLAY_MODE_STORAGE_KEY), "configured"); + assert.equal(storage.getItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY), null); +}); + +test("writeProviderDisplayModePreference stores 'compact' and cleans old key", () => { + const storage = makeMockStorage(); + writeProviderDisplayModePreference("compact", storage); + assert.equal(storage.getItem(PROVIDER_DISPLAY_MODE_STORAGE_KEY), "compact"); + assert.equal(storage.getItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY), null); +}); + +test("writeProviderDisplayModePreference with 'all' removes all keys", () => { + const storage = makeMockStorage(); + storage.setItem(PROVIDER_DISPLAY_MODE_STORAGE_KEY, "configured"); + storage.setItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY, "true"); + + writeProviderDisplayModePreference("all", storage); + assert.equal(storage.getItem(PROVIDER_DISPLAY_MODE_STORAGE_KEY), null); + assert.equal(storage.getItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY), null); +}); + +// --------------------------------------------------------------------------- +// shouldSyncProviderDisplayMode — race guard (#5510) +// The persistence effects must stay inert while the connections fetch is still +// loading; otherwise an empty connections list coerces a saved "configured" +// preference to "all" and the filter is lost across reloads. +// --------------------------------------------------------------------------- + +test("shouldSyncProviderDisplayMode blocks the effects while loading (#5510 race guard)", () => { + // The bug: without the loading check, the effect runs against connections.length === 0 + // mid-fetch and overwrites the saved preference. This is the failing-without-fix case. + assert.equal(shouldSyncProviderDisplayMode(true, true), false); +}); + +test("shouldSyncProviderDisplayMode stays inert until the stored preference is read", () => { + assert.equal(shouldSyncProviderDisplayMode(false, false), false); + assert.equal(shouldSyncProviderDisplayMode(false, true), false); +}); + +test("shouldSyncProviderDisplayMode allows persistence once ready and settled", () => { + assert.equal(shouldSyncProviderDisplayMode(true, false), true); +}); diff --git a/tests/unit/db-adapters/nodeSqliteShared.test.ts b/tests/unit/db-adapters/nodeSqliteShared.test.ts new file mode 100644 index 0000000000..fe9d653e75 --- /dev/null +++ b/tests/unit/db-adapters/nodeSqliteShared.test.ts @@ -0,0 +1,100 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { createNodeSqliteAdapterFromDatabase } from "../../../src/lib/db/adapters/nodeSqliteShared.ts"; + +type Row = Record; + +class FakeStatement { + finalized = false; + + constructor( + private readonly sql: string, + private readonly rows: Row[] = [] + ) {} + + run(..._params: unknown[]) { + return { changes: 1n, lastInsertRowid: 7n }; + } + + get(..._params: unknown[]) { + if (this.sql.startsWith("PRAGMA")) { + return { journal_mode: "wal" }; + } + return this.rows[0]; + } + + all(..._params: unknown[]) { + if (this.sql.startsWith("PRAGMA")) { + return [{ journal_mode: "wal" }]; + } + return this.rows; + } + + finalize() { + this.finalized = true; + } +} + +class FakeDb { + closed = false; + execCalls: string[] = []; + statements: FakeStatement[] = []; + + prepare(sql: string) { + const statement = new FakeStatement(sql, [{ value: "ok" }]); + this.statements.push(statement); + return statement; + } + + exec(sql: string) { + this.execCalls.push(sql); + } + + close() { + this.closed = true; + } +} + +test("createNodeSqliteAdapterFromDatabase wraps node:sqlite statements", () => { + const db = new FakeDb(); + const adapter = createNodeSqliteAdapterFromDatabase(db, ":memory:"); + + const insert = adapter.prepare("INSERT INTO t VALUES (?)").run("ok"); + assert.deepEqual(insert, { changes: 1, lastInsertRowid: 7 }); + + const row = adapter.prepare("SELECT value FROM t").get() as Row; + assert.equal(row.value, "ok"); + + assert.equal(adapter.pragma("journal_mode", { simple: true }), "wal"); + assert.deepEqual(adapter.pragma("journal_mode"), [{ journal_mode: "wal" }]); +}); + +test("createNodeSqliteAdapterFromDatabase uses savepoints for transactions", () => { + const db = new FakeDb(); + const adapter = createNodeSqliteAdapterFromDatabase(db, ":memory:"); + + const run = adapter.transaction((value: string) => value.toUpperCase()); + assert.equal(run("ok"), "OK"); + assert.equal(db.execCalls[0].startsWith("SAVEPOINT "), true); + assert.equal(db.execCalls[1].startsWith("RELEASE "), true); +}); + +test("createNodeSqliteAdapterFromDatabase finalizes cached statements on close", () => { + const db = new FakeDb(); + let closedHookCalls = 0; + const adapter = createNodeSqliteAdapterFromDatabase(db, ":memory:", () => { + closedHookCalls++; + }); + + adapter.prepare("SELECT 1").get(); + adapter.close(); + + assert.equal(closedHookCalls, 1); + assert.equal(db.closed, true); + assert.equal(adapter.open, false); + assert.equal( + db.statements.every((statement) => statement.finalized), + true + ); +}); diff --git a/tests/unit/db-import-ebusy-5406.test.ts b/tests/unit/db-import-ebusy-5406.test.ts new file mode 100644 index 0000000000..8279607e45 --- /dev/null +++ b/tests/unit/db-import-ebusy-5406.test.ts @@ -0,0 +1,37 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +// Regression guard for #5406: the database-import route deleted the live +// storage.sqlite + WAL/-shm/-journal sidecars with a plain synchronous +// `fs.unlinkSync` and no retry. On Windows the OS releases the SQLite file +// handle asynchronously after `db.close()` (mmap / antivirus), so the immediate +// unlink races and throws EBUSY. The restore path already solved this with +// `unlinkFileWithRetry` (EBUSY/EPERM backoff); the import path must use the +// same helper instead of raw `fs.unlinkSync`. + +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = join(here, "..", ".."); +const importRoute = join(repoRoot, "src/app/api/db-backups/import/route.ts"); + +test("#5406: import route uses unlinkFileWithRetry (EBUSY-safe on Windows)", () => { + const src = readFileSync(importRoute, "utf8"); + assert.match( + src, + /unlinkFileWithRetry/, + "import route must delete the sqlite files via unlinkFileWithRetry (EBUSY retry)" + ); +}); + +test("#5406: import route does not raw-unlink the live sqlite files (EBUSY race)", () => { + const src = readFileSync(importRoute, "utf8"); + // The buggy code deleted the sqlite + WAL sidecars with `fs.unlinkSync(filePath)` + // inside the sqliteFilesToReplace loop. Only that path races to EBUSY; the + // temp-upload cleanup (`fs.unlinkSync(tmpPath)`) is a different, unlocked file. + assert.ok( + !/fs\.unlinkSync\s*\(\s*filePath\b/.test(src), + "the sqlite-replace loop must use unlinkFileWithRetry, not raw fs.unlinkSync(filePath)" + ); +}); diff --git a/tests/unit/design-grid-background.test.ts b/tests/unit/design-grid-background.test.ts index 34da2ad84e..7c12b99990 100644 --- a/tests/unit/design-grid-background.test.ts +++ b/tests/unit/design-grid-background.test.ts @@ -98,6 +98,7 @@ test("Card / Modal / Input / Select adopt the radius scale and border token", () test("status colors come from one canonical module", () => { const mod = read("../../src/shared/constants/statusColors.ts"); + assert.match(mod, /export const STATUS_HEX/); assert.match(mod, /success:\s*"#22c55e"/); assert.match(mod, /warning:\s*"#f59e0b"/); assert.match(mod, /error:\s*"#ef4444"/); diff --git a/tests/unit/discontinued-providers-2026.test.ts b/tests/unit/discontinued-providers-2026.test.ts index 0af6bf5e2f..79fef1ead5 100644 --- a/tests/unit/discontinued-providers-2026.test.ts +++ b/tests/unit/discontinued-providers-2026.test.ts @@ -9,18 +9,23 @@ import assert from "node:assert"; describe("2026 discontinued free tiers — providers.ts hasFree reconciliation", () => { it("APIKEY_PROVIDERS dead tiers no longer advertise a free tier", async () => { const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts"); - for (const id of ["chutes", "kluster", "glhf", "phind", "gitlawb", "gitlawb-gmi", "aimlapi", "yi"]) { + // These providers still operate (an API key works) but lost their free tier, so + // they are KEPT with hasFree:false. phind is NOT here: the whole phind.com service + // shut down 2026-01-16, so it was removed entirely (registry/executor/catalogs), + // matching the dead-service-removal precedent (#5246 Gemini CLI). + for (const id of ["chutes", "kluster", "glhf", "gitlawb", "gitlawb-gmi", "aimlapi", "yi"]) { const p = (APIKEY_PROVIDERS as Record)[id]; assert.ok(p, `${id} should still exist in APIKEY_PROVIDERS (provider not removed, only its free flag)`); assert.strictEqual(p.hasFree, false, `${id} should have hasFree:false (discontinued in 2026)`); } }); - it("WEB_COOKIE_PROVIDERS phind (web/cookie path) no longer advertises a free tier", async () => { - const { WEB_COOKIE_PROVIDERS } = await import("../../src/shared/constants/providers.ts"); - const p = (WEB_COOKIE_PROVIDERS as Record)["phind"]; - assert.ok(p, "phind should still exist in WEB_COOKIE_PROVIDERS"); - assert.strictEqual(p.hasFree, false, "phind web/cookie should have hasFree:false (phind.com shut down 2026-01)"); + it("phind is fully removed (service shut down 2026-01) from both catalogs", async () => { + const { APIKEY_PROVIDERS, WEB_COOKIE_PROVIDERS } = await import( + "../../src/shared/constants/providers.ts" + ); + assert.ok(!("phind" in APIKEY_PROVIDERS), "phind must not be in APIKEY_PROVIDERS"); + assert.ok(!("phind" in WEB_COOKIE_PROVIDERS), "phind must not be in WEB_COOKIE_PROVIDERS"); }); it("intentionally-kept providers still advertise free (genuinely free / ToS-flagged, not flipped)", async () => { diff --git a/tests/unit/error-message-sanitization.test.ts b/tests/unit/error-message-sanitization.test.ts index 6c4a1d7192..a3059e8a67 100644 --- a/tests/unit/error-message-sanitization.test.ts +++ b/tests/unit/error-message-sanitization.test.ts @@ -18,6 +18,9 @@ const mappingsRoute = await import("../../src/app/api/model-combo-mappings/route const mappingsIdRoute = await import("../../src/app/api/model-combo-mappings/[id]/route.ts"); const syncTokens = await import("../../src/lib/sync/tokens.ts"); +const repoRoot = path.resolve(import.meta.dirname, "../.."); +const read = (relPath: string) => fs.readFileSync(path.join(repoRoot, relPath), "utf8"); + function makeRequest(url: string, options: { method?: string; body?: unknown } = {}) { const { method = "GET", body } = options; return new Request(url, { @@ -240,6 +243,13 @@ test("buildErrorBody never exposes stack traces in its message", async () => { assert.ok(!body.error.message.includes("at /opt")); }); +test("types barrel keeps the model cooldown payload export only", async () => { + const src = await read("src/types/index.ts"); + assert.match(src, /ModelCooldownErrorPayload/); + assert.doesNotMatch(src, /ProviderConnection/); + assert.doesNotMatch(src, /ProviderNode/); +}); + // ── sanitizeUpstreamDetails ────────────────────────────────────────────────── test("sanitizeUpstreamDetails — basic pass-through for safe fields", async () => { @@ -342,6 +352,21 @@ test("createErrorResult — exposes error code/type on the result object", async assert.equal(result.errorType, "timeout"); }); +test("buildModelCooldownBody returns the public cooldown error payload shape", async () => { + const { buildModelCooldownBody } = await import("../../open-sse/utils/error.ts"); + const body = buildModelCooldownBody({ model: "gpt-4o", retryAfterSec: 1.2 }); + + assert.deepEqual(body, { + error: { + message: "All credentials for model gpt-4o are cooling down", + type: "rate_limit_error", + code: "model_cooldown", + model: "gpt-4o", + reset_seconds: 2, + }, + }); +}); + test("regression: upstream_details never contains stack trace text", async () => { const { createErrorResult } = await import("../../open-sse/utils/error.ts"); const upstream = { error: { message: "err" }, stack: "Error\n at /abs/path.ts:1:2" }; diff --git a/tests/unit/executor-phind.test.ts b/tests/unit/executor-phind.test.ts deleted file mode 100644 index da1a844cdc..0000000000 --- a/tests/unit/executor-phind.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { describe, it } from "node:test"; -import assert from "node:assert/strict"; - -const mod = await import("../../open-sse/executors/phind.ts"); - -describe("PhindExecutor", () => { - it("can be instantiated", () => { - const executor = new mod.PhindExecutor(); - assert.ok(executor); - }); - - it("execute returns proper shape on missing cookie (fetch fails)", async () => { - const executor = new mod.PhindExecutor(); - try { - const result = await executor.execute({ - model: "phind-model", - body: { messages: [{ role: "user", content: "hi" }] }, - stream: false, - credentials: { apiKey: "" }, - signal: null, - }); - assert.ok(result.response instanceof Response); - assert.ok(typeof result.url === "string"); - assert.ok(typeof result.headers === "object"); - } catch { - // Network error expected in test env - } - }); - - it("execute builds correct URL", async () => { - const executor = new mod.PhindExecutor(); - try { - const result = await executor.execute({ - model: "test", - body: { messages: [{ role: "user", content: "hi" }] }, - stream: false, - credentials: { apiKey: "fake" }, - signal: null, - }); - assert.ok(result.url.includes("phind.com/api/agent")); - } catch { - // expected - } - }); -}); diff --git a/tests/unit/executor-web-cookie-sweep.test.ts b/tests/unit/executor-web-cookie-sweep.test.ts index 49050e4a8d..94bcd67f63 100644 --- a/tests/unit/executor-web-cookie-sweep.test.ts +++ b/tests/unit/executor-web-cookie-sweep.test.ts @@ -60,7 +60,6 @@ const FAKE_CREDS: Record = { "inner-ai": "fake-audit-sweep user@example.com", "adapta-web": "__client=fake-audit-sweep", huggingchat: "hf-chat=fake-audit-sweep", - phind: "fake-audit-sweep", "poe-web": "p-b=fake-audit-sweep", "venice-web": "fake-audit-sweep", "v0-vercel-web": "fake-audit-sweep", diff --git a/tests/unit/formatting-public-surface.test.ts b/tests/unit/formatting-public-surface.test.ts index 134bb8e92c..20ae941442 100644 --- a/tests/unit/formatting-public-surface.test.ts +++ b/tests/unit/formatting-public-surface.test.ts @@ -18,5 +18,8 @@ test("formatting utilities public surface excludes removed display helpers", () test("sse logger wrapper no longer re-exports formatting maskKey", () => { assert.equal(Object.hasOwn(sseLogger, "maskKey"), false); assert.equal(typeof sseLogger.debug, "function"); + assert.equal(typeof sseLogger.info, "function"); assert.equal(typeof sseLogger.warn, "function"); + assert.equal(typeof sseLogger.error, "function"); + assert.equal(typeof sseLogger.request, "function"); }); diff --git a/tests/unit/free-tier-catalog.test.ts b/tests/unit/free-tier-catalog.test.ts index ad10a85257..b0845e015b 100644 --- a/tests/unit/free-tier-catalog.test.ts +++ b/tests/unit/free-tier-catalog.test.ts @@ -12,9 +12,11 @@ test("FREE_TIER_BUDGETS holds positive integer monthly-token budgets", () => { assert.ok(Number.isInteger(tokens) && tokens > 0, `${id} must be a positive integer`); } assert.equal(FREE_TIER_BUDGETS.mistral, 1_000_000_000); - assert.equal(FREE_TIER_BUDGETS.longcat, 150_000_000); assert.equal(FREE_TIER_BUDGETS["cloudflare-ai"], 122_000_000); assert.equal(FREE_TIER_BUDGETS.cerebras, 30_000_000); + // LongCat is excluded from this recurring-monthly catalog: its free tier is a + // one-time 10M-token signup grant (not recurring), so it must not appear here. + assert.equal(FREE_TIER_BUDGETS.longcat, undefined); }); test("FREE_TIER_TOS marks proxy-prohibited providers as avoid", () => { @@ -25,16 +27,16 @@ test("FREE_TIER_TOS marks proxy-prohibited providers as avoid", () => { test("computeFreeTierTotals sums the documented budgets", () => { const t = computeFreeTierTotals(); - assert.equal(t.providerCount, 22); - assert.ok(t.documentedMonthlyTokens >= 1_500_000_000); - assert.ok(t.documentedMonthlyTokens <= 1_600_000_000); + assert.equal(t.providerCount, 21); + assert.ok(t.documentedMonthlyTokens >= 1_350_000_000); + assert.ok(t.documentedMonthlyTokens <= 1_450_000_000); assert.equal(typeof t.headline, "string"); - assert.match(t.headline, /1\.5/); + assert.match(t.headline, /1\.3/); }); test("computeFreeTierTotals can exclude ToS-avoid providers", () => { const all = computeFreeTierTotals(); const clean = computeFreeTierTotals({ excludeTosAvoid: true }); assert.equal(all.documentedMonthlyTokens - clean.documentedMonthlyTokens, 25_000); - assert.equal(clean.providerCount, 21); + assert.equal(clean.providerCount, 20); }); diff --git a/tests/unit/gamification/streaks.test.ts b/tests/unit/gamification/streaks.test.ts index 3304cbe98b..0b2e6bf557 100644 --- a/tests/unit/gamification/streaks.test.ts +++ b/tests/unit/gamification/streaks.test.ts @@ -22,5 +22,15 @@ describe("Streak Tracker", () => { const second = await updateStreak("test-user-2"); assert.equal(first, second); }); + + it("stores date metadata when starting a streak", async () => { + await updateStreak("test-user-3"); + + const streak = await getStreak("test-user-3"); + assert.equal(streak.currentStreak, 1); + assert.equal(streak.longestStreak, 1); + assert.match(streak.lastActiveDate, /^\d{4}-\d{2}-\d{2}$/); + assert.equal(streak.streakStartDate, streak.lastActiveDate); + }); }); }); diff --git a/tests/unit/headroom-proxy-lifecycle.test.ts b/tests/unit/headroom-proxy-lifecycle.test.ts index 487ea71982..f16af0b18b 100644 --- a/tests/unit/headroom-proxy-lifecycle.test.ts +++ b/tests/unit/headroom-proxy-lifecycle.test.ts @@ -6,6 +6,7 @@ import { parsePortFromHeadroomUrl, buildHeadroomStatus, } from "../../src/lib/headroom/detect"; +import { getManagedPid, isPidAlive } from "../../src/lib/headroom/process"; import { isLocalOnlyPath } from "../../src/server/authz/routeGuard"; // ────────────────────────────────────────────────────────────────────────────── @@ -88,3 +89,9 @@ test("buildHeadroomStatus: nothing installed and proxy unreachable → all false assert.equal(status.localUrl, true); assert.equal(status.canStart, false); }); + +test("process status helpers are safe for absent managed processes", () => { + assert.equal(isPidAlive(null), false); + assert.equal(isPidAlive(undefined), false); + assert.equal(getManagedPid(), null); +}); diff --git a/tests/unit/lib/batches/schemas.test.ts b/tests/unit/lib/batches/schemas.test.ts index 0c3fa7f9fc..4cdbdad921 100644 --- a/tests/unit/lib/batches/schemas.test.ts +++ b/tests/unit/lib/batches/schemas.test.ts @@ -3,6 +3,11 @@ import assert from "node:assert/strict"; const { wizardDestinationSchema, wizardCsvMappingSchema, csvToJsonlInputSchema } = await import("../../../../src/lib/batches/schemas.ts"); +const { BATCH_SUPPORTED_PROVIDERS } = await import("../../../../src/lib/batches/types.ts"); + +test("batch types module keeps the supported providers catalog exported", () => { + assert.deepEqual(BATCH_SUPPORTED_PROVIDERS, ["openai", "anthropic", "gemini"]); +}); // ── wizardDestinationSchema ────────────────────────────────────────────────── diff --git a/tests/unit/limiter-lifecycle.test.ts b/tests/unit/limiter-lifecycle.test.ts index c7d2609cc6..7d71dbbabf 100644 --- a/tests/unit/limiter-lifecycle.test.ts +++ b/tests/unit/limiter-lifecycle.test.ts @@ -202,3 +202,35 @@ test("after 429 teardown, next withRateLimit must get a fresh limiter and succee ); assert.equal(result, "post-429", "post-429 request must return its value"); }); + +test("request queue refresh treats zero limits as unbounded for existing limiters", async () => { + await flushBackgroundWork(); + + const provider = "openai"; + const connectionId = "lifecycle-test-conn-d"; + + rateLimitManager.enableRateLimitProtection(connectionId); + assert.equal( + await rateLimitManager.withRateLimit( + provider, + connectionId, + null, + async () => "before-refresh" + ), + "before-refresh" + ); + + await rateLimitManager.applyRequestQueueSettings({ + enabled: true, + autoEnableApiKeyProviders: false, + maxWaitMs: 100, + requestsPerMinute: 0, + concurrentRequests: 0, + minTimeBetweenRequestsMs: 0, + }); + + assert.equal( + await rateLimitManager.withRateLimit(provider, connectionId, null, async () => "after-refresh"), + "after-refresh" + ); +}); diff --git a/tests/unit/local-redis-runtime.test.ts b/tests/unit/local-redis-runtime.test.ts new file mode 100644 index 0000000000..7898495a79 --- /dev/null +++ b/tests/unit/local-redis-runtime.test.ts @@ -0,0 +1,55 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { + REDIS_CONTAINER_NAME, + detectRedisContainerRuntime, + redisRuntimeUnavailableResponse, + runRedisRuntimeCommand, +} from "../../src/app/api/local/redis/redisRuntime.ts"; + +test("detectRedisContainerRuntime returns the first available runtime", async () => { + const calls: string[] = []; + const runtime = await detectRedisContainerRuntime(async (file) => { + calls.push(file); + if (file === "podman") { + throw new Error("missing"); + } + return { stdout: "Docker version 1\n", stderr: "" }; + }); + + assert.equal(runtime, "docker"); + assert.deepEqual(calls, ["podman", "docker"]); +}); + +test("detectRedisContainerRuntime returns null when no runtime is available", async () => { + const runtime = await detectRedisContainerRuntime(async () => { + throw new Error("missing"); + }); + + assert.equal(runtime, null); +}); + +test("runRedisRuntimeCommand trims command output", async () => { + const result = await runRedisRuntimeCommand( + "docker", + ["stop", REDIS_CONTAINER_NAME], + 15_000, + async () => ({ + stdout: " stopped \n", + stderr: " warning \n", + }) + ); + + assert.deepEqual(result, { stdout: "stopped", stderr: "warning" }); +}); + +test("redisRuntimeUnavailableResponse preserves the route error shape", async () => { + const response = redisRuntimeUnavailableResponse(); + + assert.equal(response.status, 503); + assert.deepEqual(await response.json(), { + ok: false, + error: "No container runtime (podman or docker) found on PATH", + }); +}); diff --git a/tests/unit/logger-redaction-wiring.test.ts b/tests/unit/logger-redaction-wiring.test.ts index 09d855a4a9..8424356980 100644 --- a/tests/unit/logger-redaction-wiring.test.ts +++ b/tests/unit/logger-redaction-wiring.test.ts @@ -12,7 +12,8 @@ process.env.APP_LOG_TO_FILE = "true"; process.env.APP_LOG_FILE_PATH = logFile; process.env.APP_LOG_LEVEL = "debug"; -const { createLogger } = await import("../../src/shared/utils/logger.ts"); +const loggerModule = await import("../../src/shared/utils/logger.ts"); +const { createLogger } = loggerModule; /** Poll the (worker-thread-written) log file until the predicate holds or timeout. */ async function readLogWhen( @@ -38,10 +39,18 @@ test("logger redacts a Bearer secret in a free-form message and an error stack ( log.error({ err }, "request failed"); const contents = await readLogWhen( - (c) => c.includes("[REDACTED]") && !c.includes("sk-superSecretKey") && !c.includes("sk-anotherSecret") + (c) => + c.includes("[REDACTED]") && + !c.includes("sk-superSecretKey") && + !c.includes("sk-anotherSecret") ); assert.match(contents, /\[REDACTED\]/, "redaction marker must appear in the log output"); assert.doesNotMatch(contents, /sk-superSecretKey/, "message secret must be redacted"); assert.doesNotMatch(contents, /sk-anotherSecret/, "error-stack secret must be redacted"); }); + +test("shared logger module exposes named logger helpers", () => { + assert.equal(typeof loggerModule.logger.info, "function"); + assert.equal(typeof loggerModule.createLogger, "function"); +}); diff --git a/tests/unit/memory-schemas-roundtrip.test.ts b/tests/unit/memory-schemas-roundtrip.test.ts index cabdef7b57..29cdfbdb5f 100644 --- a/tests/unit/memory-schemas-roundtrip.test.ts +++ b/tests/unit/memory-schemas-roundtrip.test.ts @@ -19,6 +19,17 @@ import { QdrantHealthResultSchema, } from "../../src/shared/schemas/qdrant.ts"; +test("memory schema module keeps the runtime request/response schemas exported", () => { + assert.equal(typeof MemorySettingsExtendedSchema.safeParse, "function"); + assert.equal(typeof MemoryUpdatePutSchema.safeParse, "function"); + assert.equal(typeof RetrievePreviewSchema.safeParse, "function"); + assert.equal(typeof MemoryReindexSchema.safeParse, "function"); + assert.equal(typeof MemorySummarizeSchema.safeParse, "function"); + assert.equal(typeof EmbeddingProviderListingSchema.safeParse, "function"); + assert.equal(typeof MemoryEngineStatusSchema.safeParse, "function"); + assert.equal(typeof RetrievePreviewResultSchema.safeParse, "function"); +}); + // --------------------------------------------------------------------------- // 1. MemorySettingsExtendedSchema // --------------------------------------------------------------------------- @@ -66,6 +77,9 @@ test("MemorySettingsExtendedSchema: rejects invalid embeddingSource value", () = test("MemoryUpdatePutSchema: accepts valid partial update (content only)", () => { const result = MemoryUpdatePutSchema.safeParse({ content: "updated content" }); assert.equal(result.success, true, "Should accept partial update with only content"); + if (result.success) { + assert.equal(result.data.content, "updated content"); + } }); test("MemoryUpdatePutSchema: rejects extra field (strict)", () => { @@ -325,6 +339,7 @@ test("QdrantSettingsSchema: accepts valid settings with defaults applied", () => if (result.success) { assert.equal(result.data.port, 6333, "Default port should be 6333"); assert.equal(result.data.collection, "omniroute_memory", "Default collection"); + assert.equal(result.data.quantization, "none", "Default quantization should be none"); assert.equal(result.data.hasApiKey, false, "Default hasApiKey should be false"); assert.equal(result.data.apiKeyMasked, null, "Default apiKeyMasked should be null"); } diff --git a/tests/unit/mimocode-executor.test.ts b/tests/unit/mimocode-executor.test.ts index b3e0641de6..d56a499887 100644 --- a/tests/unit/mimocode-executor.test.ts +++ b/tests/unit/mimocode-executor.test.ts @@ -265,22 +265,21 @@ describe("mimocode per-account proxy", () => { assert.strictEqual(config.proxy?.host, "proxy.example.com"); }); - it("default account has null proxy", () => { - const accounts = (exec as any).accounts; - assert.ok(Array.isArray(accounts)); - assert.ok(accounts.length >= 1); - assert.strictEqual(accounts[0].proxy, null); + it("default proxyUrlMap is empty", () => { + const testExec = new MimocodeExecutor(); + const map = (testExec as any).proxyUrlMap; + assert.ok(map instanceof Map); + assert.strictEqual(map.size, 0); }); - it("syncAccountsFromCredentials reads accountProxies", () => { + it("syncAccountsFromCredentials populates proxyUrlMap with correct URLs", () => { const testExec = new MimocodeExecutor(); const fp1 = "fingerprint-1"; const fp2 = "fingerprint-2"; (testExec as any).accounts = [ - { fingerprint: fp1, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, proxy: null }, - { fingerprint: fp2, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, proxy: null }, + { fingerprint: fp1, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0 }, + { fingerprint: fp2, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0 }, ]; - (testExec as any).nextAccountIdx = 0; const credentials = { providerSpecificData: { @@ -292,42 +291,52 @@ describe("mimocode per-account proxy", () => { }; (testExec as any).syncAccountsFromCredentials(credentials); - const accounts = (testExec as any).accounts; - const acct1 = accounts.find((a: any) => a.fingerprint === fp1); - const acct2 = accounts.find((a: any) => a.fingerprint === fp2); - assert.deepStrictEqual(acct1.proxy, { type: "http", host: "p1.example.com", port: 1080 }); - assert.strictEqual(acct2.proxy, null); + const map: Map = (testExec as any).proxyUrlMap; + assert.strictEqual(map.get(fp1), "http://p1.example.com:1080"); + assert.strictEqual(map.has(fp2), false); }); it("syncAccountsFromCredentials skips when accountProxies absent", () => { const testExec = new MimocodeExecutor(); - const before = JSON.parse(JSON.stringify((testExec as any).accounts)); + const mapBefore = (testExec as any).proxyUrlMap.size; (testExec as any).syncAccountsFromCredentials({ providerSpecificData: {} }); - const after = (testExec as any).accounts; - assert.strictEqual(after.length, before.length); - assert.strictEqual(after[0].proxy, null); + assert.strictEqual((testExec as any).proxyUrlMap.size, mapBefore); }); - it("syncAccountsFromCredentials skips unknown fingerprints", () => { + it("syncAccountsFromCredentials adds proxyUrlMap entries for all valid proxy configs", () => { const testExec = new MimocodeExecutor(); const existingFp = (testExec as any).accounts[0].fingerprint; (testExec as any).syncAccountsFromCredentials({ providerSpecificData: { accountProxies: [ - { fingerprint: "nonexistent-fingerprint", proxy: { type: "socks5", host: "s5.example.com", port: 1080 } }, + { + fingerprint: "nonexistent-fingerprint", + proxy: { type: "socks5", host: "s5.example.com", port: 1080 }, + }, ], }, }); - assert.strictEqual((testExec as any).accounts[0].proxy, null); + const map: Map = (testExec as any).proxyUrlMap; + assert.strictEqual( + map.has("nonexistent-fingerprint"), + true, + "proxyUrlMap stores all valid proxy configs" + ); + assert.strictEqual(map.get("nonexistent-fingerprint"), "socks5://s5.example.com:1080"); + assert.strictEqual( + map.has(existingFp), + false, + "existing fingerprint without proxy is not in map" + ); }); - it("accounts with different proxies are tracked independently", () => { + it("accounts with different proxies produce distinct URLs", () => { const testExec = new MimocodeExecutor(); const fp1 = "fp-a"; const fp2 = "fp-b"; (testExec as any).accounts = [ - { fingerprint: fp1, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, proxy: null }, - { fingerprint: fp2, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, proxy: null }, + { fingerprint: fp1, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0 }, + { fingerprint: fp2, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0 }, ]; (testExec as any).syncAccountsFromCredentials({ providerSpecificData: { @@ -338,50 +347,141 @@ describe("mimocode per-account proxy", () => { }, }); - const accounts = (testExec as any).accounts; - const a1 = accounts.find((a: any) => a.fingerprint === fp1); - const a2 = accounts.find((a: any) => a.fingerprint === fp2); - assert.notDeepStrictEqual(a1.proxy, a2.proxy); - assert.strictEqual(a1.proxy?.host, "a.com"); - assert.strictEqual(a2.proxy?.host, "b.com"); + const map: Map = (testExec as any).proxyUrlMap; + assert.strictEqual(map.get(fp1), "http://a.com:8080"); + assert.strictEqual(map.get(fp2), "socks5://b.com:1080"); }); - it("getJwtForAccount reads proxy from account", async () => { + it("getProxyDispatcher returns a dispatcher for known fingerprint", () => { const testExec = new MimocodeExecutor(); - const fp = "test-fp-proxy"; - const proxyConfig = { type: "http", host: "proxy.test", port: 3128 }; + const fp = "fp-dispatcher-test"; (testExec as any).accounts = [ - { fingerprint: fp, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, proxy: proxyConfig }, + { fingerprint: fp, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0 }, ]; - (testExec as any).nextAccountIdx = 0; + (testExec as any).syncAccountsFromCredentials({ + providerSpecificData: { + accountProxies: [ + { fingerprint: fp, proxy: { type: "socks5", host: "s5.test", port: 1080 } }, + ], + }, + }); - const acct = (testExec as any).accounts[0]; - assert.deepStrictEqual(acct.proxy, proxyConfig); - assert.strictEqual(acct.jwt, ""); + const dispatcher = (testExec as any).getProxyDispatcher(fp); + assert.ok(dispatcher, "dispatcher should exist for registered fingerprint"); }); - it("proxy field persists on account after sync", () => { + it("getProxyDispatcher returns undefined for unknown fingerprint", () => { const testExec = new MimocodeExecutor(); - const fp = "test-fp-persist"; + const dispatcher = (testExec as any).getProxyDispatcher("unknown-fp"); + assert.strictEqual(dispatcher, undefined); + }); + + it("fetchWithProxy falls back to plain fetch when no proxy configured", async () => { + const testExec = new MimocodeExecutor(); + const fp = "fp-no-proxy"; + const originalFetch = globalThis.fetch; + let fetchCalled = false; + globalThis.fetch = async () => { + fetchCalled = true; + return new Response("ok"); + }; + try { + const resp = await (testExec as any).fetchWithProxy("https://example.com", {}, fp); + assert.ok(fetchCalled, "plain fetch should have been called"); + assert.strictEqual(resp.status, 200); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it("authenticated proxy includes credentials in URL", () => { + const testExec = new MimocodeExecutor(); + const fp = "fp-auth"; (testExec as any).accounts = [ - { fingerprint: fp, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, proxy: null }, + { fingerprint: fp, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0 }, ]; - (testExec as any).nextAccountIdx = 0; - - const proxy1 = { type: "http", host: "first.proxy", port: 8080 }; (testExec as any).syncAccountsFromCredentials({ providerSpecificData: { - accountProxies: [{ fingerprint: fp, proxy: proxy1 }], + accountProxies: [ + { + fingerprint: fp, + proxy: { + type: "socks5", + host: "s5.auth.com", + port: 1080, + username: "user", + password: "pass", + }, + }, + ], }, }); - assert.deepStrictEqual((testExec as any).accounts[0].proxy, proxy1); - const proxy2 = { type: "socks5", host: "second.proxy", port: 1080 }; + const map: Map = (testExec as any).proxyUrlMap; + const url = map.get(fp); + assert.ok(url); + assert.ok(url.includes("user:pass@"), "URL should include encoded credentials"); + }); + + it("default port is 1080 for socks5 when not specified", () => { + const testExec = new MimocodeExecutor(); + const fp = "fp-default-port"; + (testExec as any).accounts = [ + { fingerprint: fp, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0 }, + ]; (testExec as any).syncAccountsFromCredentials({ providerSpecificData: { - accountProxies: [{ fingerprint: fp, proxy: proxy2 }], + accountProxies: [ + { fingerprint: fp, proxy: { type: "socks5", host: "s5.test", port: undefined } }, + ], }, }); - assert.deepStrictEqual((testExec as any).accounts[0].proxy, proxy2); + + const map: Map = (testExec as any).proxyUrlMap; + assert.strictEqual(map.get(fp), "socks5://s5.test:1080"); + }); + + it("default port is 8080 for http when not specified", () => { + const testExec = new MimocodeExecutor(); + const fp = "fp-http-default"; + (testExec as any).accounts = [ + { fingerprint: fp, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0 }, + ]; + (testExec as any).syncAccountsFromCredentials({ + providerSpecificData: { + accountProxies: [ + { fingerprint: fp, proxy: { type: "http", host: "h.test", port: undefined } }, + ], + }, + }); + + const map: Map = (testExec as any).proxyUrlMap; + assert.strictEqual(map.get(fp), "http://h.test:8080"); + }); + + it("proxy URL map updates correctly on re-sync", () => { + const testExec = new MimocodeExecutor(); + const fp = "fp-re-sync"; + (testExec as any).accounts = [ + { fingerprint: fp, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0 }, + ]; + + (testExec as any).syncAccountsFromCredentials({ + providerSpecificData: { + accountProxies: [ + { fingerprint: fp, proxy: { type: "http", host: "first.proxy", port: 8080 } }, + ], + }, + }); + assert.strictEqual((testExec as any).proxyUrlMap.get(fp), "http://first.proxy:8080"); + + (testExec as any).syncAccountsFromCredentials({ + providerSpecificData: { + accountProxies: [ + { fingerprint: fp, proxy: { type: "socks5", host: "second.proxy", port: 1080 } }, + ], + }, + }); + assert.strictEqual((testExec as any).proxyUrlMap.get(fp), "socks5://second.proxy:1080"); }); }); diff --git a/tests/unit/model-listing-capability-5420.test.ts b/tests/unit/model-listing-capability-5420.test.ts new file mode 100644 index 0000000000..7fe0694ac3 --- /dev/null +++ b/tests/unit/model-listing-capability-5420.test.ts @@ -0,0 +1,28 @@ +// #5420 — "Import Models" must be hidden for tool-only (search/fetch) providers, +// including ones whose id does NOT end in "-search" (e.g. firecrawl → webFetch), +// while staying visible for LLM and media providers that DO list models. +import { strict as assert } from "node:assert"; +import { describe, it } from "node:test"; +import { providerLacksModelListing } from "@/lib/providers/modelListingCapability"; + +describe("providerLacksModelListing (#5420)", () => { + it("hides model listing for -search suffixed providers regardless of kinds", () => { + assert.equal(providerLacksModelListing("brave-search", []), true); + assert.equal(providerLacksModelListing("brave-search", ["webSearch"]), true); + assert.equal(providerLacksModelListing("brave-search", ["llm"]), true); + }); + + it("hides model listing for tool-only providers without the -search suffix", () => { + assert.equal(providerLacksModelListing("firecrawl", ["webFetch"]), true); + assert.equal(providerLacksModelListing("x", ["webSearch"]), true); + assert.equal(providerLacksModelListing("y", ["webSearch", "webFetch"]), true); + }); + + it("keeps model listing for LLM and media providers", () => { + assert.equal(providerLacksModelListing("openai", []), false); + assert.equal(providerLacksModelListing("openai", ["llm"]), false); + assert.equal(providerLacksModelListing("falai", ["image"]), false); + assert.equal(providerLacksModelListing("x", ["webSearch", "llm"]), false); + assert.equal(providerLacksModelListing("z", ["embedding"]), false); + }); +}); diff --git a/tests/unit/muse-spark-cookie-copy-5449.test.ts b/tests/unit/muse-spark-cookie-copy-5449.test.ts new file mode 100644 index 0000000000..825b4f1954 --- /dev/null +++ b/tests/unit/muse-spark-cookie-copy-5449.test.ts @@ -0,0 +1,41 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +// #5449: Muse Spark Web (Meta AI) migrated its default session cookie from the retired +// `abra_sess` to `ecto_1_sess` (see META_AI_DEFAULT_COOKIE), but two user-facing strings still +// named the old cookie — the provider form hint and one auth-failure message — telling users to +// paste a cookie that no longer exists. These guards keep the copy aligned with the live cookie. + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, "..", ".."); + +const webCookie = readFileSync( + join(root, "src", "shared", "constants", "providers", "web-cookie.ts"), + "utf8" +); +const executor = readFileSync(join(root, "open-sse", "executors", "muse-spark-web.ts"), "utf8"); + +test("provider form hint points at the live ecto_1_sess cookie, not retired abra_sess", () => { + assert.ok( + webCookie.includes("Paste your ecto_1_sess value"), + "muse-spark authHint must name ecto_1_sess" + ); + assert.ok( + !webCookie.includes("Paste your abra_sess"), + "muse-spark authHint must not name the retired abra_sess cookie" + ); +}); + +test("auth-failure message names the live ecto_1_sess cookie, not retired abra_sess", () => { + assert.ok( + !executor.includes("meta.ai abra_sess cookie may be missing"), + "the 401 message must not name the retired abra_sess cookie" + ); + assert.ok( + executor.includes("meta.ai ecto_1_sess cookie may be missing"), + "the 401 message must name the live ecto_1_sess cookie" + ); +}); diff --git a/tests/unit/notion-api.test.ts b/tests/unit/notion-api.test.ts index 860a5e52dd..e2d95cc780 100644 --- a/tests/unit/notion-api.test.ts +++ b/tests/unit/notion-api.test.ts @@ -45,6 +45,14 @@ test("NotionTimeoutError has correct name", () => { test("createNotionClient returns object with expected methods", async () => { const { createNotionClient } = await import("../../src/lib/notion/api.ts"); const client = createNotionClient("test-token"); + assert.deepEqual(Object.keys(client), [ + "searchPagesAndDatabases", + "getPage", + "listBlockChildren", + "queryDatabase", + "getDatabase", + "appendBlocks", + ]); assert.equal(typeof client.searchPagesAndDatabases, "function"); assert.equal(typeof client.getPage, "function"); assert.equal(typeof client.listBlockChildren, "function"); diff --git a/tests/unit/oauth-kiro.test.ts b/tests/unit/oauth-kiro.test.ts index ddf1fd62dc..35d3fe9bae 100644 --- a/tests/unit/oauth-kiro.test.ts +++ b/tests/unit/oauth-kiro.test.ts @@ -46,6 +46,11 @@ test("KIRO_CONFIG.socialClientId default matches the public CLI identifier 'kiro } }); +test("auth schemas keep the supported Kiro import schema exported", async () => { + const { kiroImportSchema } = await import("../../src/shared/validation/schemas/auth.ts"); + assert.equal(typeof kiroImportSchema.safeParse, "function"); +}); + test("Kiro social-flow routes do not duplicate the AWS auth URL or 'kiro-cli' literal", () => { // Catches future refactors that copy a hard-coded URL/identifier back into // either route. The string "kiro-cli" may still appear as an env-var name diff --git a/tests/unit/onboarding-wizard-route-5427.test.ts b/tests/unit/onboarding-wizard-route-5427.test.ts new file mode 100644 index 0000000000..a95f76f3d0 --- /dev/null +++ b/tests/unit/onboarding-wizard-route-5427.test.ts @@ -0,0 +1,36 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +// Regression guard for #5427: the "Add Provider" / onboarding route +// (src/app/(dashboard)/dashboard/providers/new/page.tsx) was a redirect stub +// that bounced to /dashboard/providers, so every wizard button silently failed +// and the fully-built ProviderOnboardingWizard component stayed orphaned +// (never imported by any route). This asserts the route renders the wizard +// instead of redirecting, and that the wizard is wired into at least one route. + +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = join(here, "..", ".."); +const newRoute = join( + repoRoot, + "src/app/(dashboard)/dashboard/providers/new/page.tsx" +); + +test("#5427: new-provider route does NOT redirect (no longer a silent stub)", () => { + const src = readFileSync(newRoute, "utf8"); + assert.ok( + !/from\s+["']next\/navigation["']/.test(src) || !/\bredirect\s*\(/.test(src), + "new/page.tsx must not call redirect() from next/navigation — that reintroduces the #5427 silent failure" + ); +}); + +test("#5427: new-provider route renders ProviderOnboardingWizard", () => { + const src = readFileSync(newRoute, "utf8"); + assert.match( + src, + /ProviderOnboardingWizard/, + "new/page.tsx must render the ProviderOnboardingWizard component" + ); +}); diff --git a/tests/unit/pack-artifact-policy.test.ts b/tests/unit/pack-artifact-policy.test.ts index cb37b2b66f..098ac4483e 100644 --- a/tests/unit/pack-artifact-policy.test.ts +++ b/tests/unit/pack-artifact-policy.test.ts @@ -64,6 +64,22 @@ test("webdav-handler.mjs is allowed in staging dist/ (server-ws.mjs dependency, assert.deepEqual(unexpectedPaths, []); }); +test("tls-options.mjs is allowed in staging dist/ (server-ws.mjs dependency, missed in 3.8.41 build — #5452)", () => { + const unexpectedPaths = findUnexpectedArtifactPaths(["tls-options.mjs"], { + exactPaths: APP_STAGING_ALLOWED_EXACT_PATHS, + prefixPaths: APP_STAGING_ALLOWED_PATH_PREFIXES, + }); + assert.deepEqual(unexpectedPaths, []); +}); + +test("dist/tls-options.mjs is a required tarball path (regression guard for #5452)", () => { + const missingPaths = findMissingArtifactPaths([], PACK_ARTIFACT_REQUIRED_PATHS); + assert.ok( + missingPaths.includes("dist/tls-options.mjs"), + "dist/tls-options.mjs must be enforced by the pack-artifact gate" + ); +}); + test("setupPolyfill.ts is allowed in the tarball (bin/omniroute.mjs imports it at startup)", () => { const unexpectedPaths = findUnexpectedArtifactPaths(["open-sse/utils/setupPolyfill.ts"], { exactPaths: PACK_ARTIFACT_ALLOWED_EXACT_PATHS, @@ -98,6 +114,7 @@ test("findMissingArtifactPaths flags missing root runtime files in the tarball", "dist/peer-stamp.mjs", "dist/responses-ws-proxy.mjs", "dist/server-ws.mjs", + "dist/tls-options.mjs", "dist/webdav-handler.mjs", "scripts/build/colocateOptionals.mjs", "scripts/build/native-binary-compat.mjs", diff --git a/tests/unit/playground-schemas.test.ts b/tests/unit/playground-schemas.test.ts index 4744ecd0b0..23ea3e7c64 100644 --- a/tests/unit/playground-schemas.test.ts +++ b/tests/unit/playground-schemas.test.ts @@ -12,6 +12,16 @@ const { StreamMetricsSchema, } = await import("../../src/shared/schemas/playground.ts"); +test("playground schema module keeps runtime schemas exported", () => { + assert.equal(typeof PlaygroundPresetRowSchema.safeParse, "function"); + assert.equal(typeof PlaygroundPresetCreateSchema.safeParse, "function"); + assert.equal(typeof PlaygroundPresetUpdateSchema.safeParse, "function"); + assert.equal(typeof PlaygroundPresetListItemSchema.safeParse, "function"); + assert.equal(typeof ToolDefinitionSchema.safeParse, "function"); + assert.equal(typeof StructuredOutputSchema.safeParse, "function"); + assert.equal(typeof StreamMetricsSchema.safeParse, "function"); +}); + // ── PlaygroundPresetRowSchema ────────────────────────────────────────────────── test("PlaygroundPresetRowSchema: valid row parses correctly", () => { @@ -30,6 +40,7 @@ test("PlaygroundPresetRowSchema: valid row parses correctly", () => { assert.equal(result.data.id, row.id); assert.equal(result.data.name, row.name); assert.equal(result.data.system, "You are helpful."); + assert.equal(result.data.params_json, row.params_json); } }); diff --git a/tests/unit/prompt-cache-prefix-analyzer.test.ts b/tests/unit/prompt-cache-prefix-analyzer.test.ts new file mode 100644 index 0000000000..2e5978e7ef --- /dev/null +++ b/tests/unit/prompt-cache-prefix-analyzer.test.ts @@ -0,0 +1,30 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; + +import { analyzePrefix, generatePromptCacheKey } from "../../src/lib/promptCache"; + +describe("prompt cache prefix analyzer", () => { + it("captures stable system prefixes and derives a cache key", () => { + const messages = [ + { role: "system", content: "You are helpful." }, + { role: "user", content: "Hello" }, + ]; + + const analysis = analyzePrefix(messages); + + assert.equal(analysis.prefixEndIdx, 0); + assert.equal(analysis.prefixType, "system_only"); + assert.equal(analysis.confidence, 0.9); + assert.ok(analysis.prefixTokens > 0); + assert.match(generatePromptCacheKey(messages), /^omni-[a-f0-9]{32}$/); + }); + + it("keeps the legacy empty-content key when there is no prefix", () => { + const messages = [{ role: "user", content: "Hello" }]; + + const analysis = analyzePrefix(messages); + + assert.equal(analysis.prefixEndIdx, -1); + assert.equal(generatePromptCacheKey(messages), "omni-e3b0c44298fc1c149afbf4c8996fb924"); + }); +}); diff --git a/tests/unit/provider-add-ux-i18n-import-warning.test.ts b/tests/unit/provider-add-ux-i18n-import-warning.test.ts new file mode 100644 index 0000000000..13add5ce32 --- /dev/null +++ b/tests/unit/provider-add-ux-i18n-import-warning.test.ts @@ -0,0 +1,60 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +import { extractImportWarning } from "../../src/app/(dashboard)/dashboard/providers/[id]/hooks/modelImportWarning.ts"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const enRaw = readFileSync( + join(__dirname, "..", "..", "src", "i18n", "messages", "en.json"), + "utf8" +); +// en.json is namespaced (keys live nested under a parent object), so scan the raw text by +// the exact `"key": "value"` pair rather than a top-level lookup. +const en = JSON.parse(enRaw); +assert.ok(en && typeof en === "object", "en.json must parse to an object"); + +// ── A1: provider-add form i18n keys must carry real copy, not the auto-generated stub +// placeholders ("Validation Model Id Label" etc.) that shipped untranslated and surfaced +// verbatim in the Add-API-Key modal (#5421/#5426/#5428/#5429/#5431/#5435/#5439). +const STUB_KEYS: Array<[string, string]> = [ + ["validationModelIdLabel", "Validation Model Id Label"], + ["validationModelIdPlaceholder", "Validation Model Id Placeholder"], + ["validationModelIdHint", "Validation Model Id Hint"], + ["accountIdLabel", "Account Id Label"], + ["accountIdPlaceholder", "Account Id Placeholder"], + ["accountIdHint", "Account Id Hint"], +]; + +for (const [key, stub] of STUB_KEYS) { + test(`en.json '${key}' is real copy, not the untranslated stub`, () => { + assert.ok(enRaw.includes(`"${key}":`), `${key} must exist in en.json`); + assert.ok( + !enRaw.includes(`"${key}": "${stub}"`), + `${key} still ships the untranslated stub "${stub}"` + ); + }); +} + +// ── A2: extractImportWarning surfaces the model-import route's `warning` field so a +// cached/local-catalog fallback is no longer silent (#5428/#5429/#5431). +test("extractImportWarning returns the warning string when present", () => { + assert.equal( + extractImportWarning({ + models: [{ id: "x" }], + warning: "API unavailable — using local catalog", + }), + "API unavailable — using local catalog" + ); +}); + +test("extractImportWarning returns null when there is no warning", () => { + assert.equal(extractImportWarning({ models: [{ id: "x" }] }), null); + assert.equal(extractImportWarning({ warning: "" }), null); + assert.equal(extractImportWarning({ warning: " " }), null); + assert.equal(extractImportWarning(null), null); + assert.equal(extractImportWarning("nope"), null); + assert.equal(extractImportWarning({ warning: 42 }), null); +}); diff --git a/tests/unit/provider-alias-uniqueness.test.ts b/tests/unit/provider-alias-uniqueness.test.ts index 7aeb68bdae..25628a84cb 100644 --- a/tests/unit/provider-alias-uniqueness.test.ts +++ b/tests/unit/provider-alias-uniqueness.test.ts @@ -18,7 +18,12 @@ import test from "node:test"; import assert from "node:assert/strict"; import { PROVIDER_ID_TO_ALIAS } from "../../open-sse/config/providerModels.ts"; -import { resolveProviderId, getProviderAlias } from "../../src/shared/constants/providers.ts"; +import { + resolveProviderId, + getProviderAlias, + APIKEY_PROVIDERS, + WEB_COOKIE_PROVIDERS, +} from "../../src/shared/constants/providers.ts"; test("no two provider IDs share the same alias in the open-sse registry", () => { const aliasToIds = new Map(); @@ -62,3 +67,19 @@ test("src/shared providers map resolves the same aliases unambiguously", () => { assert.equal(getProviderAlias("kimi"), "kimi"); assert.equal(getProviderAlias("hackclub"), "hc"); }); + +test("no provider id is registered in both the API-key and web-cookie catalogs", () => { + // A provider belongs to exactly one auth category; the same id in both catalogs + // renders the provider twice in the dashboard (once per section). huggingchat + // regressed this way (its API-key counterpart is the separate `huggingface` + // Inference API id), so it must live ONLY in WEB_COOKIE_PROVIDERS. + const apikeyIds = new Set(Object.keys(APIKEY_PROVIDERS)); + const overlap = Object.keys(WEB_COOKIE_PROVIDERS).filter((id) => apikeyIds.has(id)); + assert.deepEqual(overlap, [], `Providers duplicated across catalogs: ${overlap.join(", ")}`); + + assert.ok("huggingchat" in WEB_COOKIE_PROVIDERS, "huggingchat must be in the web-cookie catalog"); + assert.ok( + !("huggingchat" in APIKEY_PROVIDERS), + "huggingchat must NOT be in the API-key catalog (use `huggingface` for the API key path)" + ); +}); diff --git a/tests/unit/provider-endpoints-friendliai-novita.test.ts b/tests/unit/provider-endpoints-friendliai-novita.test.ts new file mode 100644 index 0000000000..4c1eca97a8 --- /dev/null +++ b/tests/unit/provider-endpoints-friendliai-novita.test.ts @@ -0,0 +1,38 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { friendliaiProvider } from "../../open-sse/config/providers/registry/friendliai/index.ts"; +import { novitaProvider } from "../../open-sse/config/providers/registry/novita/index.ts"; + +// These guards lock in two registry endpoint fixes validated live with real provider keys +// (Hard Rule #18 — real-environment test recorded in the PR): +// - FriendliAI: a serverless `flp_*` token gets 403 Forbidden on the /dedicated path; the +// /serverless path serves it. (#5430) +// - Novita: the legacy /v3 base + the typo'd `ai-ai/…` model id both 404; /openai/v1 + the +// `meta-llama/…` id return a clean OpenAI completion. (#5455) + +test("#5430 FriendliAI targets the serverless OpenAI-compatible endpoint, not dedicated", () => { + assert.ok( + friendliaiProvider.baseUrl.includes("/serverless/v1/"), + `baseUrl must use the serverless path, got: ${friendliaiProvider.baseUrl}` + ); + assert.ok( + !friendliaiProvider.baseUrl.includes("/dedicated/"), + "baseUrl must not use the dedicated path (403s serverless keys)" + ); + assert.equal(friendliaiProvider.modelsUrl, "https://api.friendli.ai/serverless/v1/models"); +}); + +test("#5455 Novita targets the /openai/v1 endpoint with a valid model id", () => { + assert.equal(novitaProvider.baseUrl, "https://api.novita.ai/openai/v1/chat/completions"); + assert.equal(novitaProvider.modelsUrl, "https://api.novita.ai/openai/v1/models"); + // The `ai-ai/` org does not exist — Novita uses `meta-llama/…`. + assert.ok( + novitaProvider.models.every((m) => !m.id.startsWith("ai-ai/")), + "Novita model ids must not use the non-existent ai-ai/ org" + ); + assert.ok( + novitaProvider.models.some((m) => m.id === "meta-llama/llama-3.1-8b-instruct"), + "Novita must list the valid meta-llama/llama-3.1-8b-instruct id" + ); +}); diff --git a/tests/unit/provider-error-rules.test.ts b/tests/unit/provider-error-rules.test.ts index aac4f820a1..c431592828 100644 --- a/tests/unit/provider-error-rules.test.ts +++ b/tests/unit/provider-error-rules.test.ts @@ -14,12 +14,9 @@ import assert from "node:assert/strict"; * - Anything else: falls back to global ERROR_RULES. */ -const { classifyError, checkFallbackError } = await import( - "../../open-sse/services/accountFallback.ts" -); -const { RateLimitReason } = await import( - "../../open-sse/config/constants.ts" -); +const { classifyError, checkFallbackError } = + await import("../../open-sse/services/accountFallback.ts"); +const { RateLimitReason } = await import("../../open-sse/config/constants.ts"); test("S1: Opencode 429 with x-ratelimit-remaining-requests=0 → QUOTA_EXHAUSTED, not RATE_LIMIT_EXCEEDED", () => { // Opencode uses account-wide quota. The header `x-ratelimit-remaining-requests: 0` @@ -43,9 +40,8 @@ test("S2: Minimax 429 with x-model-quota-remaining header → QUOTA_EXHAUSTED wi // signals ONLY that model is locked; other models on the same connection must // remain available. classifyError returns the reason; the caller (combo.ts) // reads the scope from providerRuleMatch to decide lockModel vs updateProviderConnection. - const { providerRuleRegistry, getProviderErrorRuleMatch } = await import( - "../../open-sse/config/providerErrorRules.ts" - ); + const { providerRuleRegistry, getProviderErrorRuleMatch } = + await import("../../open-sse/config/providerErrorRules.ts"); // The registry must be loaded for minimax const minimaxRules = providerRuleRegistry.get("minimax"); @@ -67,6 +63,24 @@ test("S2: Minimax 429 with x-model-quota-remaining header → QUOTA_EXHAUSTED wi ); }); +test("S2b: provider error rules match canonical-cased plain header records", async () => { + const { getProviderErrorRuleMatch } = await import("../../open-sse/config/providerErrorRules.ts"); + + const opencodeMatch = getProviderErrorRuleMatch("OpenCode", 429, { + "X-RateLimit-Remaining-Requests": "0", + }); + assert.ok(opencodeMatch, "Opencode quota headers must be case-insensitive"); + assert.equal(opencodeMatch.reason, "quota_exhausted"); + assert.equal(opencodeMatch.scope, "provider"); + + const minimaxMatch = getProviderErrorRuleMatch("Minimax", 429, { + "X-Model-Quota-Remaining": "haiku=0,sonnet=42", + }); + assert.ok(minimaxMatch, "Minimax quota headers must be case-insensitive"); + assert.equal(minimaxMatch.reason, "quota_exhausted"); + assert.equal(minimaxMatch.scope, "model"); +}); + test("S3: Regression — provider with no rules falls back to global ERROR_RULES unchanged", () => { // A provider not in the registry (e.g. "unknown-vendor") must NOT cause // classifyError to crash or return a different result. It must behave diff --git a/tests/unit/providers-constants-split.test.ts b/tests/unit/providers-constants-split.test.ts index 5888538492..eb4e425673 100644 --- a/tests/unit/providers-constants-split.test.ts +++ b/tests/unit/providers-constants-split.test.ts @@ -31,12 +31,12 @@ test("barrel still exports every catalog + key helpers", () => { } }); -test("APIKEY_PROVIDERS merges the 6 family files into 160 entries (no loss / no dup)", async () => { +test("APIKEY_PROVIDERS merges the 6 family files into 158 entries (no loss / no dup)", async () => { const keys = Object.keys((P as Record).APIKEY_PROVIDERS); - assert.equal(keys.length, 160); - assert.equal(new Set(keys).size, 160, "duplicate keys after spread-merge"); + assert.equal(keys.length, 158); + assert.equal(new Set(keys).size, 158, "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 160. + // strict partition (every provider in exactly one), so the sum must be exactly 158. const families: [string, string][] = [ ["gateways", "APIKEY_PROVIDERS_GATEWAYS"], ["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"], @@ -56,7 +56,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 160 entries (no loss / no seen.add(k); } } - assert.equal(famTotal, 160, "families must partition all 160 providers"); + assert.equal(famTotal, 158, "families must partition all 158 providers"); }); test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => { diff --git a/tests/unit/proxy-registry-route-handlers.test.ts b/tests/unit/proxy-registry-route-handlers.test.ts new file mode 100644 index 0000000000..b1b66b9c10 --- /dev/null +++ b/tests/unit/proxy-registry-route-handlers.test.ts @@ -0,0 +1,93 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +// Regression coverage for the shared proxy-route handlers extracted in #5472. +// resolveProxyLookupResponse is the single point of truth for the GET branch of +// both /api/settings/proxies (whereUsed param: "whereUsed") and +// /api/v1/management/proxies (whereUsed param: "where_used"), so its three +// branches — id+whereUsed, id-only, list (no id) — must stay equivalent across +// the parameterized callers. + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-proxy-route-handlers-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = "test-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const proxiesDb = await import("../../src/lib/db/proxies.ts"); +const { resolveProxyLookupResponse } = await import( + "../../src/lib/api/proxyRegistryRouteHandlers.ts" +); + +async function resetStorage() { + delete process.env.INITIAL_PASSWORD; + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +test.after(async () => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("resolveProxyLookupResponse returns null for the list path (no id)", async () => { + await resetStorage(); + const result = await resolveProxyLookupResponse(new URLSearchParams(), "whereUsed"); + assert.equal(result, null); +}); + +test("resolveProxyLookupResponse returns the proxy when id matches", async () => { + await resetStorage(); + const created = await proxiesDb.createProxy({ + name: "Lookup Proxy", + type: "http", + host: "127.0.0.1", + port: 8080, + }); + assert.ok(created?.id); + + const result = await resolveProxyLookupResponse( + new URLSearchParams({ id: created.id }), + "whereUsed" + ); + assert.ok(result instanceof Response); + assert.equal(result.status, 200); + const body = (await result.json()) as Record; + assert.equal(body.id, created.id); + assert.equal(body.name, "Lookup Proxy"); +}); + +test("resolveProxyLookupResponse returns 404 for an unknown id", async () => { + await resetStorage(); + const result = await resolveProxyLookupResponse( + new URLSearchParams({ id: "does-not-exist" }), + "whereUsed" + ); + assert.ok(result instanceof Response); + assert.equal(result.status, 404); +}); + +test("resolveProxyLookupResponse honors the caller's whereUsed param name (where_used)", async () => { + await resetStorage(); + const created = await proxiesDb.createProxy({ + name: "Usage Proxy", + type: "http", + host: "127.0.0.1", + port: 9090, + }); + assert.ok(created?.id); + + // management route uses the snake_case param name; the usage branch must fire. + const result = await resolveProxyLookupResponse( + new URLSearchParams({ id: created.id, where_used: "1" }), + "where_used" + ); + assert.ok(result instanceof Response); + assert.equal(result.status, 200); + // getProxyWhereUsed returns a usage payload (array/object), not the proxy row. + const body = await result.json(); + assert.notEqual((body as Record)?.name, "Usage Proxy"); +}); diff --git a/tests/unit/qdrant-schemas.test.ts b/tests/unit/qdrant-schemas.test.ts new file mode 100644 index 0000000000..ec29e09a23 --- /dev/null +++ b/tests/unit/qdrant-schemas.test.ts @@ -0,0 +1,35 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + QdrantHealthResultSchema, + QdrantSearchSchema, + QdrantSettingsSchema, + QdrantSettingsUpdateSchema, +} from "../../src/shared/schemas/qdrant.ts"; + +test("qdrant schema module keeps runtime request and response schemas exported", () => { + assert.equal(typeof QdrantSettingsSchema.safeParse, "function"); + assert.equal(typeof QdrantSettingsUpdateSchema.safeParse, "function"); + assert.equal(typeof QdrantSearchSchema.safeParse, "function"); + assert.equal(typeof QdrantHealthResultSchema.safeParse, "function"); +}); + +test("QdrantSettingsUpdateSchema validates partial updates", () => { + const result = QdrantSettingsUpdateSchema.safeParse({ + enabled: true, + host: "qdrant.local", + port: 6334, + }); + + assert.equal(result.success, true); +}); + +test("QdrantSearchSchema applies the default topK", () => { + const result = QdrantSearchSchema.safeParse({ query: "semantic search" }); + + assert.equal(result.success, true); + if (result.success) { + assert.equal(result.data.topK, 5); + } +}); diff --git a/tests/unit/quota-schemas.test.ts b/tests/unit/quota-schemas.test.ts index 85448c1505..e939dc16b1 100644 --- a/tests/unit/quota-schemas.test.ts +++ b/tests/unit/quota-schemas.test.ts @@ -20,6 +20,7 @@ test("PoolCreateSchema defaults allocations to []", () => { const r = PoolCreateSchema.safeParse({ connectionId: "c", name: "Pool" }); assert.ok(r.success); assert.deepEqual(r.data?.allocations, []); + assert.equal(r.data?.connectionId, "c"); }); test("PoolCreateSchema rejects empty name", () => { diff --git a/tests/unit/rtk-gradle-dotnet-filters.test.ts b/tests/unit/rtk-gradle-dotnet-filters.test.ts new file mode 100644 index 0000000000..ead554a589 --- /dev/null +++ b/tests/unit/rtk-gradle-dotnet-filters.test.ts @@ -0,0 +1,37 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { detectCommandType } from "../../open-sse/services/compression/engines/rtk/commandDetector.ts"; +import { matchRtkFilter } from "../../open-sse/services/compression/engines/rtk/filterLoader.ts"; + +// T07 / R9 — gradle + dotnet RTK catalog filters (the catalog gap behind kubectl, etc.). + +const GRADLE_OUTPUT = + "Welcome to Gradle 8.5!\n> Task :app:compileJava UP-TO-DATE\n> Task :app:test\nBUILD SUCCESSFUL in 12s\n"; +const DOTNET_OUTPUT = + "Microsoft (R) Build Engine version 17.8\n Determining projects to restore...\n Restored /repo/App.csproj\nBuild succeeded.\n 0 Warning(s)\n"; + +test("detectCommandType recognizes gradle and dotnet by command", () => { + assert.equal(detectCommandType("", "gradle build").type, "gradle"); + assert.equal(detectCommandType("", "./gradlew test").type, "gradle"); + assert.equal(detectCommandType("", "dotnet build").type, "dotnet"); + assert.equal(detectCommandType("", "dotnet test").type, "dotnet"); +}); + +test("detectCommandType recognizes gradle/dotnet by output content alone", () => { + assert.equal(detectCommandType(GRADLE_OUTPUT).type, "gradle"); + assert.equal(detectCommandType(DOTNET_OUTPUT).type, "dotnet"); +}); + +test("matchRtkFilter selects the gradle / dotnet builtin filter", () => { + const cases: Array<[string, string, string]> = [ + ["gradle", GRADLE_OUTPUT, "gradle build"], + ["gradle", GRADLE_OUTPUT, "./gradlew test"], + ["dotnet", DOTNET_OUTPUT, "dotnet build"], + ["dotnet", DOTNET_OUTPUT, "dotnet test"], + ]; + for (const [id, output, command] of cases) { + const filter = matchRtkFilter(output, command, { customFiltersEnabled: false }); + assert.equal(filter?.id, id, `${command} should match the ${id} filter`); + assert.equal(filter?.category, "build"); + } +}); diff --git a/tests/unit/search-registry.test.ts b/tests/unit/search-registry.test.ts index 59804d7511..d51c5fbb22 100644 --- a/tests/unit/search-registry.test.ts +++ b/tests/unit/search-registry.test.ts @@ -338,6 +338,13 @@ test("SEARCH_CACHE_DEFAULT_TTL_MS is positive", () => { // ─── Validation Schema Tests ──────────────────────────────── +test("shared validation exports the v1 search request schema", async () => { + const schemas = await import("../../src/shared/validation/schemas.ts"); + + assert.equal("v1SearchSchema" in schemas, true); + assert.equal(typeof schemas.v1SearchSchema.safeParse, "function"); +}); + test("v1SearchSchema validates correct input", async () => { const { v1SearchSchema } = await import("../../src/shared/validation/schemas.ts"); diff --git a/tests/unit/services/install-route-helper.test.ts b/tests/unit/services/install-route-helper.test.ts new file mode 100644 index 0000000000..e3df61d733 --- /dev/null +++ b/tests/unit/services/install-route-helper.test.ts @@ -0,0 +1,121 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { + handleServiceInstall, + readServiceInstallVersion, +} from "../../../src/app/api/services/_shared/installRoute.ts"; +import { InstallError } from "../../../src/lib/services/installers/utils.ts"; + +async function readJson(response: Response) { + return (await response.json()) as Record; +} + +test("readServiceInstallVersion defaults empty requests to latest", async () => { + const result = await readServiceInstallVersion( + new Request("http://localhost/api/services/example/install", { method: "POST" }) + ); + + assert.deepEqual(result, { ok: true, version: "latest" }); +}); + +test("readServiceInstallVersion returns the requested version", async () => { + const result = await readServiceInstallVersion( + new Request("http://localhost/api/services/example/install", { + method: "POST", + body: JSON.stringify({ version: "1.2.3" }), + headers: { "Content-Type": "application/json" }, + }) + ); + + assert.deepEqual(result, { ok: true, version: "1.2.3" }); +}); + +test("readServiceInstallVersion rejects malformed versions (#5495 SERVICE_VERSION_PATTERN guard)", async () => { + const result = await readServiceInstallVersion( + new Request("http://localhost/api/services/example/install", { + method: "POST", + body: JSON.stringify({ version: "../../malicious" }), + headers: { "Content-Type": "application/json" }, + }) + ); + + assert.equal(result.ok, false); + if (result.ok) throw new Error("expected version validation failure"); + assert.equal(result.response.status, 400); +}); + +test("handleServiceInstall never reaches the installer for a malformed version (#5495)", async () => { + const calls: string[] = []; + const response = await handleServiceInstall( + new Request("http://localhost/api/services/example/install", { + method: "POST", + body: JSON.stringify({ version: "v1; rm -rf /" }), + headers: { "Content-Type": "application/json" }, + }), + async (version) => { + calls.push(version); + return { installedVersion: version, installPath: "/tmp/service", durationMs: 1 }; + } + ); + + assert.deepEqual(calls, []); + assert.equal(response.status, 400); +}); + +test("readServiceInstallVersion preserves invalid JSON error shape", async () => { + const result = await readServiceInstallVersion( + new Request("http://localhost/api/services/example/install", { + method: "POST", + body: "not-json", + headers: { "Content-Type": "application/json" }, + }) + ); + + assert.equal(result.ok, false); + if (result.ok) throw new Error("expected parse failure"); + assert.equal(result.response.status, 400); + const body = await readJson(result.response); + assert.equal((body.error as Record).message, "Invalid JSON body"); +}); + +test("handleServiceInstall wraps successful installer results", async () => { + const calls: string[] = []; + const response = await handleServiceInstall( + new Request("http://localhost/api/services/example/install", { + method: "POST", + body: JSON.stringify({ version: "2.0.0" }), + headers: { "Content-Type": "application/json" }, + }), + async (version) => { + calls.push(version); + return { + installedVersion: version, + installPath: "/tmp/service", + durationMs: 42, + }; + } + ); + + assert.deepEqual(calls, ["2.0.0"]); + assert.equal(response.status, 200); + assert.deepEqual(await readJson(response), { + ok: true, + installedVersion: "2.0.0", + installPath: "/tmp/service", + durationMs: 42, + }); +}); + +test("handleServiceInstall maps InstallError to its friendly message and status", async () => { + const response = await handleServiceInstall( + new Request("http://localhost/api/services/example/install", { method: "POST" }), + async () => { + throw new InstallError("raw command failed", "Friendly install failure", 503); + } + ); + + assert.equal(response.status, 503); + const body = await readJson(response); + assert.equal((body.error as Record).message, "Friendly install failure"); +}); diff --git a/tests/unit/services/installers/ninerouter.test.ts b/tests/unit/services/installers/ninerouter.test.ts index 973f3b9f9c..fd56a9dd0d 100644 --- a/tests/unit/services/installers/ninerouter.test.ts +++ b/tests/unit/services/installers/ninerouter.test.ts @@ -24,11 +24,14 @@ set -e CMD="$1" shift if [ "$CMD" = "install" ]; then - # Find --prefix arg value + # Resolve the install prefix like real npm: an explicit --prefix arg wins, + # otherwise fall back to the npm_config_prefix env var (#5379 passes the + # prefix via env instead of argv so paths with spaces survive the win shell). PREFIX="" while [ $# -gt 0 ]; do if [ "$1" = "--prefix" ]; then PREFIX="$2"; shift 2; else shift; fi done + if [ -z "$PREFIX" ]; then PREFIX="$npm_config_prefix"; fi PKG_DIR="$PREFIX/node_modules/9router" mkdir -p "$PKG_DIR/app" echo '{"name":"9router","version":"0.4.59"}' > "$PKG_DIR/package.json" diff --git a/tests/unit/services/installers/runNpm-shell-5379.test.ts b/tests/unit/services/installers/runNpm-shell-5379.test.ts new file mode 100644 index 0000000000..0c8a9e64ad --- /dev/null +++ b/tests/unit/services/installers/runNpm-shell-5379.test.ts @@ -0,0 +1,74 @@ +/** + * Regression tests for #5379 — `spawn EINVAL` installing embedded services + * (9Router / CLIProxy) on Windows + Node.js 24+. + * + * Node 24 no longer lets `child_process.execFile()` run `.cmd` batch files on + * Windows without a shell (nodejs/node#52554). npm on Windows is `npm.cmd`, so + * `runNpm()` threw `EINVAL` immediately. The fix flips `shell` on win32. + * + * Because `shell: true` makes the shell — not execFile — parse the command line, + * NO runtime value may be interpolated into argv (Hard Rule #13). The install + * `--prefix` (a DATA_DIR path that can contain spaces, e.g. + * `C:\Users\John Doe\.omniroute\…`) is therefore passed via the + * `npm_config_prefix` environment variable instead of an argv entry, and the + * user-supplied install `version` is constrained by SERVICE_VERSION_PATTERN. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + buildNpmExecOptions, + SERVICE_VERSION_PATTERN, +} from "../../../../src/lib/services/installers/utils.ts"; + +test("buildNpmExecOptions: win32 enables shell so npm.cmd runs on Node 24 (#5379)", () => { + const opts = buildNpmExecOptions("win32", { timeoutMs: 1000 }); + assert.equal(opts.shell, true); +}); + +test("buildNpmExecOptions: non-win32 platforms never enable shell", () => { + for (const platform of ["linux", "darwin", "freebsd"] as NodeJS.Platform[]) { + const opts = buildNpmExecOptions(platform, { timeoutMs: 1000 }); + assert.equal(opts.shell, undefined, `${platform} must not use a shell`); + } +}); + +test("buildNpmExecOptions: prefix is passed via npm_config_prefix env, never argv (Hard Rule #13)", () => { + const prefix = "C:\\Users\\John Doe\\.omniroute\\services\\9router"; + const opts = buildNpmExecOptions("win32", { timeoutMs: 1000, prefix }); + assert.equal(opts.env.npm_config_prefix, prefix); +}); + +test("buildNpmExecOptions: without a prefix npm_config_prefix is left untouched", () => { + const inherited = process.env.npm_config_prefix; + const opts = buildNpmExecOptions("linux", { timeoutMs: 1000 }); + assert.equal(opts.env.npm_config_prefix, inherited); +}); + +test("buildNpmExecOptions: carries cwd, timeout and maxBuffer through", () => { + const opts = buildNpmExecOptions("linux", { cwd: "/tmp/install", timeoutMs: 4242 }); + assert.equal(opts.cwd, "/tmp/install"); + assert.equal(opts.timeout, 4242); + assert.equal(opts.maxBuffer, 10 * 1024 * 1024); +}); + +test("SERVICE_VERSION_PATTERN: accepts dist-tags and semver", () => { + for (const v of ["latest", "next", "1.2.3", "1.2.3-beta.1", "1.2.3+build.5", "0.4.59"]) { + assert.ok(SERVICE_VERSION_PATTERN.test(v), `${v} should be valid`); + } +}); + +test("SERVICE_VERSION_PATTERN: rejects shell metacharacters (injection guard)", () => { + for (const v of [ + "latest && calc", + "1.2.3; rm -rf /", + "$(whoami)", + "`id`", + "a|b", + "a b", + "", + "-flag", + ]) { + assert.equal(SERVICE_VERSION_PATTERN.test(v), false, `${JSON.stringify(v)} must be rejected`); + } +}); diff --git a/tests/unit/settings-transform-schema.test.ts b/tests/unit/settings-transform-schema.test.ts new file mode 100644 index 0000000000..a1eb6d1615 --- /dev/null +++ b/tests/unit/settings-transform-schema.test.ts @@ -0,0 +1,100 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { updateSettingsSchema } from "../../src/shared/validation/settingsSchemas.ts"; + +const commonOperations = [ + { + kind: "drop_paragraph_if_contains", + needles: ["needle"], + caseSensitive: true, + }, + { + kind: "drop_paragraph_if_starts_with", + prefixes: ["prefix"], + }, + { + kind: "replace_text", + match: "before", + replacement: "after", + allOccurrences: true, + }, + { + kind: "replace_regex", + pattern: "before", + flags: "gi", + replacement: "after", + }, + { + kind: "drop_block_if_contains", + needles: ["needle"], + }, + { + kind: "prepend_system_block", + text: "prefix block", + idempotencyKey: "prefix-key", + }, + { + kind: "append_system_block", + text: "suffix block", + }, + { + kind: "inject_billing_header", + entrypoint: "claude-code", + versionFormat: "ex-machina", + cchAlgo: "sha256-first-user", + version: "1.0.0", + }, +] as const; + +test("settings schema accepts the shared transform operations in legacy and v2 configs", () => { + const parsed = updateSettingsSchema.parse({ + ccBridgeTransforms: { + enabled: true, + pipeline: commonOperations, + }, + systemTransforms: { + providers: { + claude: { + enabled: true, + pipeline: commonOperations, + }, + }, + }, + }); + + assert.equal(parsed.ccBridgeTransforms?.pipeline.length, commonOperations.length); + assert.equal(parsed.systemTransforms?.providers.claude.pipeline.length, commonOperations.length); +}); + +test("settings schema keeps obfuscate_words limited to systemTransforms", () => { + const obfuscateWords = { + kind: "obfuscate_words", + words: ["opencode"], + targets: ["system"], + } as const; + + assert.equal( + updateSettingsSchema.safeParse({ + systemTransforms: { + providers: { + claude: { + enabled: true, + pipeline: [obfuscateWords], + }, + }, + }, + }).success, + true + ); + + assert.equal( + updateSettingsSchema.safeParse({ + ccBridgeTransforms: { + enabled: true, + pipeline: [obfuscateWords], + }, + }).success, + false + ); +}); diff --git a/tests/unit/skills-schemas.test.ts b/tests/unit/skills-schemas.test.ts new file mode 100644 index 0000000000..09ba3d7168 --- /dev/null +++ b/tests/unit/skills-schemas.test.ts @@ -0,0 +1,46 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + SkillConfigSchema, + SkillCreateInputSchema, + SkillSchema, + SkillUpdateInputSchema, +} from "../../src/lib/skills/schemas.ts"; +import { SkillMode } from "../../src/lib/skills/types.ts"; + +test("skills schema module keeps runtime schemas exported", () => { + assert.equal(typeof SkillSchema.safeParse, "function"); + assert.equal(typeof SkillCreateInputSchema.safeParse, "function"); + assert.equal(typeof SkillUpdateInputSchema.safeParse, "function"); + assert.equal(typeof SkillConfigSchema.safeParse, "function"); +}); + +test("SkillCreateInputSchema accepts a valid custom skill definition", () => { + const result = SkillCreateInputSchema.safeParse({ + name: "memory-search", + version: "1.2.3", + description: "Search memory entries", + schema: { + input: { query: "string" }, + output: { results: "array" }, + }, + handler: "export default async function run() {}", + }); + + assert.equal(result.success, true); +}); + +test("SkillConfigSchema applies defaults for execution settings", () => { + const result = SkillConfigSchema.safeParse({ + enabled: true, + mode: SkillMode.HYBRID, + allowedSkills: ["memory-search"], + }); + + assert.equal(result.success, true); + if (result.success) { + assert.equal(result.data.timeout, 30000); + assert.equal(result.data.maxRetries, 3); + } +}); diff --git a/tests/unit/skills-skillssh.test.ts b/tests/unit/skills-skillssh.test.ts index 7d72aa844e..bf9c2cc549 100644 --- a/tests/unit/skills-skillssh.test.ts +++ b/tests/unit/skills-skillssh.test.ts @@ -47,6 +47,11 @@ test.after(() => { // ── Zod schema validation tests ── +test("skills.sh module keeps runtime schemas exported", () => { + assert.equal(typeof SkillsShSkillSchema.safeParse, "function"); + assert.equal(typeof SkillsShSearchResponseSchema.safeParse, "function"); +}); + test("SkillsShSkillSchema parses a valid skill object", () => { const result = SkillsShSkillSchema.parse({ id: "supabase/agent-skills/supabase-postgres", diff --git a/tests/unit/sse-shim-contract.test.ts b/tests/unit/sse-shim-contract.test.ts index fa4717e20f..b27a500d44 100644 --- a/tests/unit/sse-shim-contract.test.ts +++ b/tests/unit/sse-shim-contract.test.ts @@ -46,6 +46,14 @@ test("src/sse model shim keeps parseModel behavior aligned with open-sse core", } }); +test("src/sse model shim exposes the active model helper surface", async () => { + const srcModel = await import("../../src/sse/services/model.ts"); + + for (const helper of ["parseModel", "getModelInfo", "getCombo", "getComboForModel"]) { + assert.equal(typeof srcModel[helper], "function", `${helper} should stay exported`); + } +}); + test("src/sse service wrappers delegate to open-sse and shared infrastructure", () => { const tokenRefreshSource = readProjectFile("src/sse/services/tokenRefresh.ts"); const modelSource = readProjectFile("src/sse/services/model.ts"); diff --git a/tests/unit/stream-handler.test.ts b/tests/unit/stream-handler.test.ts index 716d428a94..6271cd5fa5 100644 --- a/tests/unit/stream-handler.test.ts +++ b/tests/unit/stream-handler.test.ts @@ -98,6 +98,75 @@ test("createDisconnectAwareStream treats errors after OpenAI DONE as successful assert.doesNotMatch(text, /terminated/); }); +test("createDisconnectAwareStream treats cancel after OpenAI DONE as successful completion", async () => { + let disconnectHandled = false; + const transformStream = { + readable: new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode("data: [DONE]\n\n")); + }, + }), + writable: { + getWriter() { + return { + abort() {}, + }; + }, + }, + }; + + const stream = createDisconnectAwareStream( + transformStream, + createStreamController({ + onDisconnect() { + disconnectHandled = true; + }, + }) + ); + const reader = stream.getReader(); + const first = await reader.read(); + assert.equal(decoder.decode(first.value), "data: [DONE]\n\n"); + await reader.cancel("request_signal_aborted"); + + assert.equal(disconnectHandled, false); +}); + +test("createDisconnectAwareStream treats cancel after Responses completed as successful completion", async () => { + let disconnectHandled = false; + const transformStream = { + readable: new ReadableStream({ + start(controller) { + controller.enqueue( + encoder.encode('event: response.completed\ndata: {"type":"response.completed"}\n\n') + ); + }, + }), + writable: { + getWriter() { + return { + abort() {}, + }; + }, + }, + }; + + const stream = createDisconnectAwareStream( + transformStream, + createStreamController({ + clientResponseFormat: FORMATS.OPENAI_RESPONSES, + onDisconnect() { + disconnectHandled = true; + }, + }) + ); + const reader = stream.getReader(); + const first = await reader.read(); + assert.match(decoder.decode(first.value), /response\.completed/); + await reader.cancel("request_signal_aborted"); + + assert.equal(disconnectHandled, false); +}); + test("createDisconnectAwareStream: Gemini 503 high-demand error becomes SSE error chunk with message preserved", async () => { const geminiMsg = "[503]: This model is currently experiencing high demand. Spikes in demand are usually temporary. Please try again later."; diff --git a/tests/unit/token-refresh-race-comprehensive.test.ts b/tests/unit/token-refresh-race-comprehensive.test.ts index ec85c91383..070cd85601 100644 --- a/tests/unit/token-refresh-race-comprehensive.test.ts +++ b/tests/unit/token-refresh-race-comprehensive.test.ts @@ -87,6 +87,13 @@ test("Fix C reverted: codexAuthImport does NOT refresh tokens on import (avoids ); }); +test("codexAuthImport public surface keeps only consumed auth import types", async () => { + const src = await read("src/lib/oauth/utils/codexAuthImport.ts"); + assert.doesNotMatch(src, /export interface CodexAuthFileInput\b/); + assert.match(src, /export interface ParsedCodexAuth\b/); + assert.match(src, /export interface CreateConnectionOptions\b/); +}); + test("Fix D: staleness fallback returns absolute expiresAt, not raw expiresIn", async () => { const src = await read("open-sse/services/tokenRefresh.ts"); // Locate the actual return statement, not the JSDoc mention. diff --git a/tests/unit/types-barrel-model-cooldown.test.ts b/tests/unit/types-barrel-model-cooldown.test.ts new file mode 100644 index 0000000000..d96dda9ac0 --- /dev/null +++ b/tests/unit/types-barrel-model-cooldown.test.ts @@ -0,0 +1,16 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +test("types barrel supports the model cooldown error payload consumer", async () => { + const { buildModelCooldownBody } = await import("../../open-sse/utils/error.ts"); + + assert.deepEqual(buildModelCooldownBody({ model: "gpt-4o", retryAfterSec: 1.2 }), { + error: { + message: "All credentials for model gpt-4o are cooling down", + type: "rate_limit_error", + code: "model_cooldown", + model: "gpt-4o", + reset_seconds: 2, + }, + }); +}); diff --git a/tests/unit/ui/compression-settings-tab-consolidation.test.tsx b/tests/unit/ui/compression-settings-tab-consolidation.test.tsx new file mode 100644 index 0000000000..101aaf5724 --- /dev/null +++ b/tests/unit/ui/compression-settings-tab-consolidation.test.tsx @@ -0,0 +1,114 @@ +// @vitest-environment jsdom +import React from "react"; +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +// next-intl → echo the key so we can assert on stable identifiers. +vi.mock("next-intl", () => ({ + useTranslations: () => (key: string) => key, +})); + +vi.mock("next/link", () => ({ + default: ({ children, href }: { children: React.ReactNode; href: string }) => ( + {children} + ), +})); + +vi.mock("@/shared/components", () => ({ + Card: ({ children, className }: { children: React.ReactNode; className?: string }) => ( +
+ {children} +
+ ), + Button: ({ children, onClick }: { children?: React.ReactNode; onClick?: () => void }) => ( + + ), +})); + +const CONFIG = { + enabled: true, + defaultMode: "standard", + autoTriggerTokens: 0, + cacheMinutes: 5, + preserveSystemPrompt: true, + comboOverrides: {}, + cavemanConfig: { + enabled: true, + compressRoles: ["user"], + skipRules: [], + minMessageLength: 50, + preservePatterns: [], + intensity: "full", + }, + cavemanOutputMode: { enabled: false, intensity: "full", autoClarity: true }, + rtkConfig: { enabled: true, intensity: "standard" }, +}; + +let container: HTMLDivElement; +let root: ReturnType | undefined; + +beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + vi.stubGlobal( + "fetch", + vi.fn((url: string) => { + const u = String(url); + if (u.includes("/api/settings/compression")) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(CONFIG) }); + } + if (u.includes("/api/compression/rules")) { + return Promise.resolve({ ok: true, json: () => Promise.resolve({ rules: [] }) }); + } + return Promise.resolve({ ok: false, json: () => Promise.resolve(null) }); + }) + ); +}); + +afterEach(() => { + act(() => root?.unmount()); + root = undefined; + container.remove(); + vi.unstubAllGlobals(); + vi.restoreAllMocks(); +}); + +async function renderTab() { + const { default: CompressionSettingsTab } = + await import("@/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab"); + await act(async () => { + root = createRoot(container); + root.render(); + }); + // Drain the on-mount fetch → json → setState microtask chain. + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + }); +} + +describe("CompressionSettingsTab — compression controls consolidation (T11)", () => { + it("renders the read-only TokenSaver summary that links to the unified panel", async () => { + await renderTab(); + const hrefs = Array.from(container.querySelectorAll("a")).map((a) => a.getAttribute("href")); + expect(hrefs).toContain("/dashboard/context/settings"); + expect(container.textContent).toContain("tokenSaverTitle"); + }); + + it("does not render a duplicate caveman engine on/off toggle (panel owns on/off)", async () => { + await renderTab(); + const note = container.querySelector('[data-testid="caveman-panel-note"]'); + expect(note).not.toBeNull(); + // The note points users to the single-source panel... + expect(note?.textContent).toContain("/dashboard/context/settings"); + // ...and the caveman header no longer carries its own enable toggle button. + expect(note?.querySelector("button")).toBeNull(); + }); + + it("keeps the advanced caveman tuning the panel does not expose", async () => { + await renderTab(); + expect(container.textContent).toContain("compressionRoleUser"); + expect(container.textContent).toContain("compressionSkipRules"); + expect(container.textContent).toContain("compressionPreservePatterns"); + }); +}); diff --git a/tests/unit/version-manager-request.test.ts b/tests/unit/version-manager-request.test.ts new file mode 100644 index 0000000000..371f18dd8d --- /dev/null +++ b/tests/unit/version-manager-request.test.ts @@ -0,0 +1,47 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { validateVersionManagerToolBody } from "../../src/app/api/version-manager/request.ts"; + +async function readFailure(result: ReturnType) { + assert.equal(result.ok, false); + if (result.ok) { + throw new Error("expected validation failure"); + } + + return { + status: result.response.status, + body: await result.response.json(), + }; +} + +test("validateVersionManagerToolBody accepts cliproxy aliases", () => { + assert.deepEqual(validateVersionManagerToolBody({ tool: "cliproxy" }), { + ok: true, + tool: "cliproxy", + }); + assert.deepEqual(validateVersionManagerToolBody({ tool: "cliproxyapi" }), { + ok: true, + tool: "cliproxyapi", + }); +}); + +test("validateVersionManagerToolBody rejects unknown tools with the legacy response shape", async () => { + const failure = await readFailure(validateVersionManagerToolBody({ tool: "other" })); + + assert.equal(failure.status, 400); + assert.deepEqual(failure.body, { error: "Unknown tool: other" }); +}); + +test("validateVersionManagerToolBody rejects invalid bodies", async () => { + const failure = await readFailure(validateVersionManagerToolBody({})); + + assert.equal(failure.status, 400); + assert.equal(failure.body.error.message, "Invalid request"); + assert.deepEqual(failure.body.error.details, [ + { + field: "tool", + message: "Invalid input: expected string, received undefined", + }, + ]); +}); diff --git a/tests/unit/vscode-shared-metadata.test.ts b/tests/unit/vscode-shared-metadata.test.ts new file mode 100644 index 0000000000..9d8e1d0047 --- /dev/null +++ b/tests/unit/vscode-shared-metadata.test.ts @@ -0,0 +1,63 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +const familyFirstModelIds = + await import("../../src/app/api/v1/vscode/[token]/familyFirstModelIds.ts"); +const rawFamilyFirstModelIds = + await import("../../src/app/api/v1/vscode/raw/[token]/familyFirstModelIds.ts"); +const serviceTierVariants = + await import("../../src/app/api/v1/vscode/[token]/serviceTierVariants.ts"); +const rawServiceTierVariants = + await import("../../src/app/api/v1/vscode/raw/[token]/serviceTierVariants.ts"); +const reasoningMetadata = await import("../../src/app/api/v1/vscode/[token]/reasoningMetadata.ts"); +const rawReasoningMetadata = + await import("../../src/app/api/v1/vscode/raw/[token]/reasoningMetadata.ts"); + +test("vscode raw and tokenized family-first helpers share behavior", () => { + assert.equal( + familyFirstModelIds.resolveFamilyFirstPublishedModelId("gpt-5.4__provider_cx__tier_priority"), + "cx/gpt-5.4__tier_priority" + ); + assert.deepEqual( + rawFamilyFirstModelIds.getFamilyFirstModelCandidates("cx/gpt-5.4__tier_flex", "gpt-5.4"), + familyFirstModelIds.getFamilyFirstModelCandidates("cx/gpt-5.4__tier_flex", "gpt-5.4") + ); +}); + +test("vscode raw and tokenized service tier helpers share behavior", () => { + const tokenizedPayload = serviceTierVariants.resolveVscodeServiceTierRequest({ + model: "gpt-5.4__provider_cx__tier_flex", + }); + const rawPayload = rawServiceTierVariants.resolveVscodeServiceTierRequest({ + model: "gpt-5.4__provider_cx__tier_flex", + }); + + assert.deepEqual(rawPayload, tokenizedPayload); + assert.deepEqual( + serviceTierVariants.expandVscodeServiceTierModels([ + { id: "cx/gpt-5.4", name: "cx/gpt-5.4", owned_by: "codex" }, + ]), + rawServiceTierVariants.expandVscodeServiceTierModels([ + { id: "cx/gpt-5.4", name: "cx/gpt-5.4", owned_by: "codex" }, + ]) + ); +}); + +test("vscode raw and tokenized reasoning helpers share behavior", () => { + const reasoningModel = { + id: "openai/gpt-5-high", + owned_by: "openai", + capabilities: { reasoning: true }, + }; + const supportedValues = reasoningMetadata.getReasoningEffortValues(reasoningModel); + + assert.deepEqual(supportedValues, rawReasoningMetadata.getReasoningEffortValues(reasoningModel)); + assert.equal( + reasoningMetadata.inferSelectedReasoningEffort(reasoningModel, supportedValues), + "high" + ); + assert.deepEqual( + reasoningMetadata.buildReasoningConfigSchema(["none", "high"], "high"), + rawReasoningMetadata.buildReasoningConfigSchema(["none", "high"], "high") + ); +}); diff --git a/tests/unit/vscode-tokenized-request.test.ts b/tests/unit/vscode-tokenized-request.test.ts new file mode 100644 index 0000000000..69d82658d1 --- /dev/null +++ b/tests/unit/vscode-tokenized-request.test.ts @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +const tokenizedRequest = await import("../../src/app/api/v1/vscode/[token]/tokenizedRequest.ts"); +const rawTokenizedRequest = + await import("../../src/app/api/v1/vscode/raw/[token]/tokenizedRequest.ts"); + +test("vscode tokenized request helper infers grouped path token", () => { + const request = new Request("http://localhost/api/v1/vscode/sk-grouped/models"); + const rewritten = tokenizedRequest.withPathTokenApiKey(request); + + assert.equal(rewritten.headers.get("x-api-key"), "sk-grouped"); + assert.equal(rewritten.headers.get("authorization"), "Bearer sk-grouped"); +}); + +test("vscode tokenized request helper infers raw path token", () => { + const request = new Request("http://localhost/api/v1/vscode/raw/sk-raw/models"); + const rewritten = rawTokenizedRequest.withPathTokenApiKey(request); + + assert.equal(rewritten.headers.get("x-api-key"), "sk-raw"); + assert.equal(rewritten.headers.get("authorization"), "Bearer sk-raw"); +}); + +test("vscode tokenized request helper preserves existing auth headers", () => { + const request = new Request("http://localhost/api/v1/vscode/sk-path/models", { + headers: { + authorization: "Bearer existing", + "x-api-key": "sk-existing", + }, + }); + const rewritten = tokenizedRequest.withPathTokenApiKey(request, "sk-explicit"); + + assert.equal(rewritten.headers.get("x-api-key"), "sk-existing"); + assert.equal(rewritten.headers.get("authorization"), "Bearer existing"); +}); + +test("vscode tokenized request helper carries POST bodies", async () => { + const request = new Request("http://localhost/api/v1/vscode/raw/sk-raw/chat/completions", { + method: "POST", + body: JSON.stringify({ model: "test" }), + }); + const rewritten = rawTokenizedRequest.withPathTokenApiKey(request); + + assert.equal(rewritten.headers.get("x-api-key"), "sk-raw"); + assert.deepEqual(await rewritten.json(), { model: "test" }); +}); diff --git a/tests/unit/web-cookie-providers-new.test.ts b/tests/unit/web-cookie-providers-new.test.ts index 2baa240f33..fcaff2a212 100644 --- a/tests/unit/web-cookie-providers-new.test.ts +++ b/tests/unit/web-cookie-providers-new.test.ts @@ -2,7 +2,6 @@ import test from "node:test"; import assert from "node:assert/strict"; const { HuggingChatExecutor } = await import("../../open-sse/executors/huggingchat.ts"); -const { PhindExecutor } = await import("../../open-sse/executors/phind.ts"); const { PoeWebExecutor } = await import("../../open-sse/executors/poe-web.ts"); const { VeniceWebExecutor } = await import("../../open-sse/executors/venice-web.ts"); const { V0VercelWebExecutor } = await import("../../open-sse/executors/v0-vercel-web.ts"); @@ -97,13 +96,6 @@ test("HuggingChat executor is registered", () => { assert.ok(executor instanceof HuggingChatExecutor); }); -test("Phind executor is registered", () => { - assert.ok(hasSpecializedExecutor("phind")); - assert.ok(hasSpecializedExecutor("ph")); - const executor = getExecutor("phind"); - assert.ok(executor instanceof PhindExecutor); -}); - test("Poe Web executor is registered", () => { assert.ok(hasSpecializedExecutor("poe-web")); assert.ok(hasSpecializedExecutor("poe")); @@ -149,11 +141,6 @@ test("HuggingChat sets correct provider", () => { assert.equal(executor.getProvider(), "huggingchat"); }); -test("Phind sets correct provider", () => { - const executor = new PhindExecutor(); - assert.equal(executor.getProvider(), "phind"); -}); - test("Poe Web sets correct provider", () => { const executor = new PoeWebExecutor(); assert.equal(executor.getProvider(), "poe-web"); @@ -314,41 +301,6 @@ test("HuggingChat: fetch failure returns 502", async () => { } }); -// ── Phind Execution Tests ──────────────────────────────────────────────────── - -test("Phind: streaming returns SSE chunks", async () => { - const sseData = [ - 'data: {"choices":[{"delta":{"content":"Hello "}}]}', - 'data: {"choices":[{"delta":{"content":"world"}}]}', - ]; - const restore = mockFetchCapture(200, mockSSEStream(sseData)); - try { - const executor = new PhindExecutor(); - const result = await executor.execute({ - ...noopExecuteInput, - model: "phind-model", - }); - assert.ok(result.response instanceof Response); - assert.ok(result.url.includes("phind.com")); - const text = await result.response.text(); - assert.ok(text.includes("data:")); - } finally { - restore.restore(); - } -}); - -test("Phind: error response returns error result", async () => { - const restore = mockFetchCapture(403, "Forbidden"); - try { - const executor = new PhindExecutor(); - const result = await executor.execute(noopExecuteInput); - assert.ok(result.response instanceof Response); - assert.equal(result.response.status, 403); - } finally { - restore.restore(); - } -}); - // ── Poe Web Execution Tests ────────────────────────────────────────────────── test("Poe Web: non-streaming returns JSON completion", async () => { @@ -515,7 +467,6 @@ test("Doubao Web: error response returns error result", async () => { test("All executors handle Cookie: prefix", async () => { const executors = [ new HuggingChatExecutor(), - new PhindExecutor(), new PoeWebExecutor(), new VeniceWebExecutor(), new V0VercelWebExecutor(), @@ -553,7 +504,6 @@ test("All executors handle Cookie: prefix", async () => { test("All executors handle bare cookie value", async () => { const executors = [ new HuggingChatExecutor(), - new PhindExecutor(), new PoeWebExecutor(), new VeniceWebExecutor(), new V0VercelWebExecutor(), diff --git a/tests/unit/webhook-deliveries-db.test.ts b/tests/unit/webhook-deliveries-db.test.ts index a57708e9f3..c98ba9cf7a 100644 --- a/tests/unit/webhook-deliveries-db.test.ts +++ b/tests/unit/webhook-deliveries-db.test.ts @@ -42,6 +42,7 @@ test("insertDelivery stores a row and getDeliveries returns it", () => { assert.equal(rows[0].http_status, 200); assert.equal(rows[0].latency_ms, 142); assert.equal(rows[0].status, "delivered"); + assert.equal("payload_snapshot" in rows[0], false); }); test("rotation keeps only last 100 deliveries per webhook", () => {
🚫 Never hit limits
Auto-fallback across 237 providers in milliseconds. Quota out? Next provider takes over — zero downtime.
🚫 Never hit limits
Auto-fallback across 236 providers in milliseconds. Quota out? Next provider takes over — zero downtime.
💸 Save up to 95% tokens
RTK + Caveman stacked compression cuts 15–95% of eligible tokens (~89% avg on tool-heavy sessions).
🆓 $0 to start
50+ providers with a free tier, 11 free forever (Kiro, Qoder, Pollinations, LongCat…). No card needed.
AgentRouter
GPT-5, Claude, Gemini
$100 free credits
Qoder AI
Kimi-K2, DeepSeek-R1
Unlimited FREE
Pollinations
GPT-5, Claude, Llama 4
No key needed
LongCat
Flash-Lite
50M tokens/day 🔥
LongCat
LongCat-2.0
10M tokens one-time (KYC) 🔑
Cloudflare AI
50+ models
10K neurons/day